diff --git a/.gitignore b/.gitignore index 0f0791c..b6ae769 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,5 @@ llm/* embedding/* pyrightconfig.json +loader/tmp_files +flagged/* \ No newline at end of file diff --git a/README.md b/README.md index 9a7afb4..993c8c7 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ docker run --gpus all -d --name chatglm -p 7860:7860 -v ~/github/langchain-ChatG 本项目已在 Python 3.8 - 3.10,CUDA 11.7 环境下完成测试。已在 Windows、ARM 架构的 macOS、Linux 系统中完成测试。 vue前端需要node18环境 + ### 从本地加载模型 请参考 [THUDM/ChatGLM-6B#从本地加载模型](https://github.com/THUDM/ChatGLM-6B#从本地加载模型) @@ -177,6 +178,7 @@ Web UI 可以实现如下功能: - [ ] Langchain 应用 - [x] 接入非结构化文档(已支持 md、pdf、docx、txt 文件格式) + - [x] jpg 与 png 格式图片的 OCR 文字识别 - [ ] 搜索引擎与本地网页接入 - [ ] 结构化数据接入(如 csv、Excel、SQL 等) - [ ] 知识图谱/图数据库接入 @@ -200,6 +202,7 @@ Web UI 可以实现如下功能: - [ ] 增加知识库管理 - [x] 选择知识库开始问答 - [x] 上传文件/文件夹至知识库 + - [x] 知识库测试 - [ ] 删除知识库中文件 - [ ] 利用 streamlit 实现 Web UI Demo - [ ] 增加 API 支持 diff --git a/chains/local_doc_qa.py b/chains/local_doc_qa.py index 66d9d29..a454931 100644 --- a/chains/local_doc_qa.py +++ b/chains/local_doc_qa.py @@ -10,6 +10,8 @@ import numpy as np from utils import torch_gc from tqdm import tqdm from pypinyin import lazy_pinyin +from loader import UnstructuredPaddleImageLoader +from loader import UnstructuredPaddlePDFLoader DEVICE_ = EMBEDDING_DEVICE DEVICE_ID = "0" if torch.cuda.is_available() else None @@ -21,16 +23,31 @@ def load_file(filepath, sentence_size=SENTENCE_SIZE): loader = UnstructuredFileLoader(filepath, mode="elements") docs = loader.load() elif filepath.lower().endswith(".pdf"): - loader = UnstructuredFileLoader(filepath, strategy="fast") + loader = UnstructuredPaddlePDFLoader(filepath) textsplitter = ChineseTextSplitter(pdf=True, sentence_size=sentence_size) docs = loader.load_and_split(textsplitter) + elif filepath.lower().endswith(".jpg") or filepath.lower().endswith(".png"): + loader = UnstructuredPaddleImageLoader(filepath, mode="elements") + textsplitter = ChineseTextSplitter(pdf=False, sentence_size=sentence_size) + docs = loader.load_and_split(text_splitter=textsplitter) else: loader = UnstructuredFileLoader(filepath, mode="elements") textsplitter = ChineseTextSplitter(pdf=False, sentence_size=sentence_size) docs = loader.load_and_split(text_splitter=textsplitter) + write_check_file(filepath, docs) return docs +def write_check_file(filepath, docs): + fout = open('load_file.txt', 'a') + fout.write("filepath=%s,len=%s" % (filepath, len(docs))) + fout.write('\n') + for i in docs: + fout.write(str(i)) + fout.write('\n') + fout.close() + + def generate_prompt(related_docs: List[str], query: str, prompt_template=PROMPT_TEMPLATE) -> str: context = "\n".join([doc.page_content for doc in related_docs]) @@ -212,7 +229,7 @@ class LocalDocQA: if not vs_path or not one_title or not one_conent: logger.info("知识库添加错误,请确认知识库名字、标题、内容是否正确!") return None, [one_title] - docs = [Document(page_content=one_conent+"\n", metadata={"source": one_title})] + docs = [Document(page_content=one_conent + "\n", metadata={"source": one_title})] if not one_content_segmentation: text_splitter = ChineseTextSplitter(pdf=False, sentence_size=sentence_size) docs = text_splitter.split_documents(docs) diff --git a/content/langchain-ChatGLM_README.md b/content/samples/README.md similarity index 85% rename from content/langchain-ChatGLM_README.md rename to content/samples/README.md index cbf40e6..9a7afb4 100644 --- a/content/langchain-ChatGLM_README.md +++ b/content/samples/README.md @@ -31,6 +31,10 @@ ## 硬件需求 - ChatGLM-6B 模型硬件需求 + + 注:如未将模型下载至本地,请执行前检查`$HOME/.cache/huggingface/`文件夹剩余空间,模型文件下载至本地需要 15 GB 存储空间。 + + 模型下载方法可参考 [常见问题](docs/FAQ.md) 中 Q8。 | **量化等级** | **最低 GPU 显存**(推理) | **最低 GPU 显存**(高效参数微调) | | -------------- | ------------------------- | --------------------------------- | @@ -38,6 +42,17 @@ | INT8 | 8 GB | 9 GB | | INT4 | 6 GB | 7 GB | +- MOSS 模型硬件需求 + + 注:如未将模型下载至本地,请执行前检查`$HOME/.cache/huggingface/`文件夹剩余空间,模型文件下载至本地需要 70 GB 存储空间 + + 模型下载方法可参考 [常见问题](docs/FAQ.md) 中 Q8。 + + | **量化等级** | **最低 GPU 显存**(推理) | **最低 GPU 显存**(高效参数微调) | + |-------------------|-----------------------| --------------------------------- | + | FP16(无量化) | 68 GB | - | + | INT8 | 20 GB | - | + - Embedding 模型硬件需求 本项目中默认选用的 Embedding 模型 [GanymedeNil/text2vec-large-chinese](https://huggingface.co/GanymedeNil/text2vec-large-chinese/tree/main) 约占用显存 3GB,也可修改为在 CPU 中运行。 @@ -66,6 +81,7 @@ docker run --gpus all -d --name chatglm -p 7860:7860 -v ~/github/langchain-ChatG 本项目已在 Python 3.8 - 3.10,CUDA 11.7 环境下完成测试。已在 Windows、ARM 架构的 macOS、Linux 系统中完成测试。 +vue前端需要node18环境 ### 从本地加载模型 请参考 [THUDM/ChatGLM-6B#从本地加载模型](https://github.com/THUDM/ChatGLM-6B#从本地加载模型) @@ -97,19 +113,31 @@ $ python webui.py ```shell $ python api.py ``` +或成功部署 API 后,执行以下脚本体验基于 VUE 的前端页面 +```shell +$ cd views +$ pnpm i -注:如未将模型下载至本地,请执行前检查`$HOME/.cache/huggingface/`文件夹剩余空间,至少15G。 +$ npm run dev +``` 执行后效果如下图所示: -![webui](img/webui_0419.png) +1. `对话` Tab 界面 +![](img/webui_0510_0.png) +2. `知识库测试 Beta` Tab 界面 +![](img/webui_0510_1.png) +3. `模型配置` Tab 界面 +![](img/webui_0510_2.png) + Web UI 可以实现如下功能: -1. 运行前自动读取`configs/model_config.py`中`LLM`及`Embedding`模型枚举及默认模型设置运行模型,如需重新加载模型,可在 `模型配置` 标签页重新选择后点击 `重新加载模型` 进行模型加载; +1. 运行前自动读取`configs/model_config.py`中`LLM`及`Embedding`模型枚举及默认模型设置运行模型,如需重新加载模型,可在 `模型配置` Tab 重新选择后点击 `重新加载模型` 进行模型加载; 2. 可手动调节保留对话历史长度、匹配知识库文段数量,可根据显存大小自行调节; -3. 具备模式选择功能,可选择 `LLM对话` 与 `知识库问答` 模式进行对话,支持流式对话; +3. `对话` Tab 具备模式选择功能,可选择 `LLM对话` 与 `知识库问答` 模式进行对话,支持流式对话; 4. 添加 `配置知识库` 功能,支持选择已有知识库或新建知识库,并可向知识库中**新增**上传文件/文件夹,使用文件上传组件选择好文件后点击 `上传文件并加载知识库`,会将所选上传文档数据加载至知识库中,并基于更新后知识库进行问答; -5. 后续版本中将会增加对知识库的修改或删除,及知识库中已导入文件的查看。 +5. 新增 `知识库测试 Beta` Tab,可用于测试不同文本切分方法与检索相关度阈值设置,暂不支持将测试参数作为 `对话` Tab 设置参数。 +6. 后续版本中将会增加对知识库的修改或删除,及知识库中已导入文件的查看。 ### 常见问题 @@ -159,6 +187,7 @@ Web UI 可以实现如下功能: - [x] [THUDM/chatglm-6b-int4](https://huggingface.co/THUDM/chatglm-6b-int4) - [x] [THUDM/chatglm-6b-int4-qe](https://huggingface.co/THUDM/chatglm-6b-int4-qe) - [x] [ClueAI/ChatYuan-large-v2](https://huggingface.co/ClueAI/ChatYuan-large-v2) + - [x] [fnlp/moss-moon-003-sft](https://huggingface.co/fnlp/moss-moon-003-sft) - [ ] 增加更多 Embedding 模型支持 - [x] [nghuyong/ernie-3.0-nano-zh](https://huggingface.co/nghuyong/ernie-3.0-nano-zh) - [x] [nghuyong/ernie-3.0-base-zh](https://huggingface.co/nghuyong/ernie-3.0-base-zh) @@ -178,6 +207,6 @@ Web UI 可以实现如下功能: - [ ] 实现调用 API 的 Web UI Demo ## 项目交流群 -![二维码](img/qr_code_14.jpg) +![二维码](img/qr_code_17.jpg) 🎉 langchain-ChatGLM 项目交流群,如果你也对本项目感兴趣,欢迎加入群聊参与讨论交流。 diff --git a/content/samples/test.jpg b/content/samples/test.jpg new file mode 100644 index 0000000..70c199b Binary files /dev/null and b/content/samples/test.jpg differ diff --git a/content/samples/test.pdf b/content/samples/test.pdf new file mode 100644 index 0000000..3a137ad Binary files /dev/null and b/content/samples/test.pdf differ diff --git a/content/test.txt b/content/samples/test.txt similarity index 100% rename from content/test.txt rename to content/samples/test.txt diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 88739c9..48abee7 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -29,7 +29,14 @@ $ git clone https://github.com/imClumsyPanda/langchain-ChatGLM.git # 进入目录 $ cd langchain-ChatGLM +# 项目中 pdf 加载由先前的 detectron2 替换为使用 paddleocr,如果之前有安装过 detectron2 需要先完成卸载避免引发 tools 冲突 +$ pip uninstall detectron2 + # 安装依赖 $ pip install -r requirements.txt + +# 验证paddleocr是否成功,首次运行会下载约18M模型到~/.paddleocr +$ python loader/image_loader.py + ``` 注:使用 `langchain.document_loaders.UnstructuredFileLoader` 进行非结构化文件接入时,可能需要依据文档进行其他依赖包的安装,请参考 [langchain 文档](https://python.langchain.com/en/latest/modules/indexes/document_loaders/examples/unstructured_file.html)。 diff --git a/docs/test.pdf b/docs/test.pdf new file mode 100644 index 0000000..3a137ad Binary files /dev/null and b/docs/test.pdf differ diff --git a/img/test.jpg b/img/test.jpg new file mode 100644 index 0000000..70c199b Binary files /dev/null and b/img/test.jpg differ diff --git a/loader/__init__.py b/loader/__init__.py new file mode 100644 index 0000000..e9a7aea --- /dev/null +++ b/loader/__init__.py @@ -0,0 +1,2 @@ +from .image_loader import UnstructuredPaddleImageLoader +from .pdf_loader import UnstructuredPaddlePDFLoader diff --git a/loader/image_loader.py b/loader/image_loader.py new file mode 100644 index 0000000..1013e82 --- /dev/null +++ b/loader/image_loader.py @@ -0,0 +1,37 @@ +"""Loader that loads image files.""" +from typing import List + +from langchain.document_loaders.unstructured import UnstructuredFileLoader +from paddleocr import PaddleOCR +import os + + +class UnstructuredPaddleImageLoader(UnstructuredFileLoader): + """Loader that uses unstructured to load image files, such as PNGs and JPGs.""" + + def _get_elements(self) -> List: + def image_ocr_txt(filepath, dir_path="tmp_files"): + full_dir_path = os.path.join(os.path.dirname(filepath), dir_path) + if not os.path.exists(full_dir_path): + os.makedirs(full_dir_path) + filename = os.path.split(filepath)[-1] + ocr = PaddleOCR(lang="ch", use_gpu=False, show_log=False) + result = ocr.ocr(img=filepath) + + ocr_result = [i[1][0] for line in result for i in line] + txt_file_path = os.path.join(full_dir_path, "%s.txt" % (filename)) + with open(txt_file_path, 'w', encoding='utf-8') as fout: + fout.write("\n".join(ocr_result)) + return txt_file_path + + txt_file_path = image_ocr_txt(self.file_path) + from unstructured.partition.text import partition_text + return partition_text(filename=txt_file_path, **self.unstructured_kwargs) + + +if __name__ == "__main__": + filepath = "../content/samples/test.jpg" + loader = UnstructuredPaddleImageLoader(filepath, mode="elements") + docs = loader.load() + for doc in docs: + print(doc) diff --git a/loader/pdf_loader.py b/loader/pdf_loader.py new file mode 100644 index 0000000..a27eec1 --- /dev/null +++ b/loader/pdf_loader.py @@ -0,0 +1,53 @@ +"""Loader that loads image files.""" +from typing import List + +from langchain.document_loaders.unstructured import UnstructuredFileLoader +from paddleocr import PaddleOCR +import os +import fitz + + +class UnstructuredPaddlePDFLoader(UnstructuredFileLoader): + """Loader that uses unstructured to load image files, such as PNGs and JPGs.""" + + def _get_elements(self) -> List: + def pdf_ocr_txt(filepath, dir_path="tmp_files"): + full_dir_path = os.path.join(os.path.dirname(filepath), dir_path) + if not os.path.exists(full_dir_path): + os.makedirs(full_dir_path) + filename = os.path.split(filepath)[-1] + ocr = PaddleOCR(lang="ch", use_gpu=False, show_log=False) + doc = fitz.open(filepath) + txt_file_path = os.path.join(full_dir_path, "%s.txt" % (filename)) + img_name = os.path.join(full_dir_path, '.tmp.png') + with open(txt_file_path, 'w', encoding='utf-8') as fout: + + for i in range(doc.page_count): + page = doc[i] + text = page.get_text("") + fout.write(text) + fout.write("\n") + + img_list = page.get_images() + for img in img_list: + pix = fitz.Pixmap(doc, img[0]) + + pix.save(img_name) + + result = ocr.ocr(img_name) + ocr_result = [i[1][0] for line in result for i in line] + fout.write("\n".join(ocr_result)) + os.remove(img_name) + return txt_file_path + + txt_file_path = pdf_ocr_txt(self.file_path) + from unstructured.partition.text import partition_text + return partition_text(filename=txt_file_path, **self.unstructured_kwargs) + + +if __name__ == "__main__": + filepath = "../content/samples/test.pdf" + loader = UnstructuredPaddlePDFLoader(filepath, mode="elements") + docs = loader.load() + for doc in docs: + print(doc) diff --git a/requirements.txt b/requirements.txt index d7b2e4e..31d44e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ +pymupdf +paddlepaddle==2.4.2 +paddleocr langchain==0.0.146 transformers==4.27.1 unstructured[local-inference] diff --git a/test_image.py b/test_image.py new file mode 100644 index 0000000..ed60890 --- /dev/null +++ b/test_image.py @@ -0,0 +1,12 @@ +from configs.model_config import * +import nltk + +nltk.data.path = [NLTK_DATA_PATH] + nltk.data.path + +filepath = "./img/test.jpg" +from loader import UnstructuredPaddleImageLoader + +loader = UnstructuredPaddleImageLoader(filepath, mode="elements") +docs = loader.load() +for doc in docs: + print(doc) diff --git a/test_pdf.py b/test_pdf.py new file mode 100644 index 0000000..32dcb34 --- /dev/null +++ b/test_pdf.py @@ -0,0 +1,12 @@ +from configs.model_config import * +import nltk + +nltk.data.path = [NLTK_DATA_PATH] + nltk.data.path + +filepath = "docs/test.pdf" +from loader import UnstructuredPaddlePDFLoader + +loader = UnstructuredPaddlePDFLoader(filepath, mode="elements") +docs = loader.load() +for doc in docs: + print(doc)