背景

本文主要介绍一下,依据Langchain与Vicuna-13B的外挂OceanBase常识库项目实战以及QA运用,项目地址:

github.com/csunny/DB-G…

在开端之前,咱们仍是先看看作用~

auto_sql.gif

自Meta发布LLaMA大模型以来, 围绕LLaMA微调的模型也是层出不穷。 从alpaca 到 vicuna,再到刚刚发布的中医垂直范畴微调大模型华佗GPT, 可谓是风景无限。 但其中最出名、作用最好的当属vicuna-13B。如下图所示,当时在众多大模型傍边,Vicuna-13B的作用非常接近ChatGPT,有其92%的作用。 这意味着什么呢? 意味着,咱们依据开源的Vicuna-13B即可搞定决大多数的使命与需求。 当然什么外挂常识库QA这样的简略需求自然不在话下。

那Langchain又是什么呢?

毫无疑问,Langchain是现在大语言模型范畴最炙手可热的LLM框架。

LangChain 是一个构建在LLM之上的运用开发框架。想让运用变得更强大,愈加不同,单单经过调用大模型的API必定是不够的, 还需求有以下特性:

  1. 数据思想: 连接大模型到其他的元数据上。
  2. 代理思想: 语言模型能够与环境交互

以上便是Langchain的规划理念, It’s very simple, but enough nature. 是的,足够简略,但很贴近实质。咱们也是被Langchain的理念深深的招引。 所以,咱们来了~

计划

既然是一个实战项目,那么在项目开端之前,咱们有必要对项目全体的架构做一个清晰的整理。

增加图片注释,不超越 140 字(可选)

如图所示,是咱们全体的架构图。 从图中咱们能够看到,左边有一条线是常识库 -> Embedding -> 向量存储 -> 大模型(Vicuna-13B) -> Generate 的途径。 在咱们本文中,便是依赖此途径外挂常识库进行推理、总结,以完成QA的工作。

所以咱们全体将以上过程拆分为如下所示的四个步骤。

  1. 常识库预备: 好像所示中,由于咱们是面向DB范畴的GPT,所以咱们预备了干流数据库的文档,并进行了分类。

  2. Embedding: embedding这一步是需求将文本转化成向量进行存储,当然了,存储媒介是向量数据库,关于向量数据库的了解,咱们能够从这儿了解向量数据库

  3. Embedding之后的常识,会存储在向量数据库傍边,用于后边的检索。

  4. 利用大模型的能力,经过ICL(In-Context-Learning) 让大模型完成依据现有常识的推理、总结。

  5. 这样咱们就能够完成一个依据现有常识库QA的项目了。

整个常识库的处理过程,也能够参阅Langchain-ChatGLM项目中的一张图。

图片来自Langchain-ChatGLM

代码阐明

既然是实战,那必定少不了代码,毕竟咱们一贯坚持的是:

Talk is cheap, show me the code.

模型加载

现在根本干流的模型都是依据HuggingFace的规范,所以模型加载代码其实就变得很简略了。 如下所示,为模型加载类,所需求的参数只需求传一个model_path, 在这个类傍边,咱们完成了一个办法,loader办法,经过这个办法咱们能够获得两个目标。 1. tokenizer 2. model, 依据这两个目标,咱们就得到一个模型了,后边的工作,重视运用就能够啦。

class ModelLoader:
    """Model loader is a class for model load
      Args: model_path
    """
    kwargs = {}
    def __init__(self, 
                 model_path) -> None:
                     self.device = "cuda" if torch.cuda.is_available() else "cpu"
                     self.model_path = model_path 
                     self.kwargs = {
                         "torch_dtype": torch.float16,
                         "device_map": "auto",
                     }
    def loader(self, num_gpus, load_8bit=False, debug=False):
        if self.device == "cpu":
            kwargs = {}
        elif self.device == "cuda":
            kwargs = {"torch_dtype": torch.float16}
            if num_gpus == "auto":
                kwargs["device_map"] = "auto"
            else:
                num_gpus = int(num_gpus)
                if num_gpus != 1:
                    kwargs.update({
                        "device_map": "auto",
                        "max_memory": {i: "13GiB" for i in range(num_gpus)},
                    })
        else:
            raise ValueError(f"Invalid device: {self.device}")
        if "chatglm" in self.model_path:
            tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True)
            model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).half().cuda()
        else:
            tokenizer = AutoTokenizer.from_pretrained(self.model_path, use_fast=False)
            model = AutoModelForCausalLM.from_pretrained(self.model_path,
                                                         low_cpu_mem_usage=True, **kwargs)
        if load_8bit:
            compress_module(model, self.device)
        if (self.device == "cuda" and num_gpus == 1):
            model.to(self.device)
        if debug:
            print(model)
        return model, tokenizer

常识库预备

预备常识库,没什么特别需求讲的,能够是pdf、txt、md等等的吧。 在这儿,咱们预备的是一个md文档,常识库是依据开源的OceanBase官方文档。 预备好的常识库地址: OceanBase文档。

注:这儿特别阐明一下,为什么没有直接下载pdf。 两个原因 1. OB pdf文档有很多的格局,这些格局在向量处理的过程中也会保存下来, 默许处理后的常识没有压扁平,不利于后续的大模型运用。 2. pdf文档相对比较大,在本地跑,经过模型抽向量的过程会比较长,因而咱们预备了一个简略的MarkDown文件来做演示。

常识转向量并存储到向量数据库

这儿咱们完成了一个Knownledge2Vector的类。这个类顾命思意,便是把常识库转化为向量。 当然咱们转化成向量之后会耐久化到数据库存储。 (问题1: 类名没有体现存数据库,是不是应该在斟酌一下?KnownLedge2VectorStore会更好? 🤔)

class KnownLedge2Vector:
    """KnownLedge2Vector class is order to load document to vector 
    and persist to vector store.
        Args: 
           - model_name
        Usage:
            k2v = KnownLedge2Vector()
            persist_dir = os.path.join(VECTORE_PATH, ".vectordb") 
            print(persist_dir)
            for s, dc in k2v.query("what is oceanbase?"):
                print(s, dc.page_content, dc.metadata)
    """
    embeddings: object = None 
    model_name = LLM_MODEL_CONFIG["sentence-transforms"]
    top_k: int = VECTOR_SEARCH_TOP_K
    def __init__(self, model_name=None) -> None:
        if not model_name:
            # use default embedding model
            self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name) 
    def init_vector_store(self):
        persist_dir = os.path.join(VECTORE_PATH, ".vectordb")
        print("向量数据库耐久化地址: ", persist_dir)
        if os.path.exists(persist_dir):
            # 从本地耐久化文件中Load
            print("从本地向量加载数据...")
            vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings)
            # vector_store.add_documents(documents=documents)
        else:
            documents = self.load_knownlege()
            # 从头初始化
            vector_store = Chroma.from_documents(documents=documents, 
                                                 embedding=self.embeddings,
                                                 persist_directory=persist_dir)
            vector_store.persist()
        return vector_store 
    def load_knownlege(self):
        docments = []
        for root, _, files in os.walk(DATASETS_DIR, topdown=False):
            for file in files:
                filename = os.path.join(root, file)
                docs = self._load_file(filename)
                # 更新metadata数据
                new_docs = [] 
                for doc in docs:
                    doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} 
                    print("文档2向量初始化中, 请稍等...", doc.metadata)
                    new_docs.append(doc)
                docments += new_docs
        return docments
    def _load_file(self, filename):
        # 加载文件
        if filename.lower().endswith(".pdf"):
            loader = UnstructuredFileLoader(filename) 
            text_splitor = CharacterTextSplitter()
            docs = loader.load_and_split(text_splitor)
        else:
            loader = UnstructuredFileLoader(filename, mode="elements")
            text_splitor = CharacterTextSplitter()
            docs = loader.load_and_split(text_splitor)
        return docs
    def _load_from_url(self, url):
        """Load data from url address"""
        pass
    def query(self, q):
        """Query similar doc from Vector """
        vector_store = self.init_vector_store()
        docs = vector_store.similarity_search_with_score(q, k=self.top_k)
        for doc in docs:
            dc, s = doc
            yield s, dc

这个类的运用也非常简略, 首先实例化,参数也是只要一个model_name, 需求留意的是,这儿的model_name 是转向量的模型,跟咱们前面的大模型不是同一个,当然这儿能不能是同一个,当然也是能够的。(问题2: 能够考虑一下,这儿咱们为什么没有选择LLM抽向量?)

这个类里边咱们干的工作其实也不多,总结一下就那么3件。 1. 读文件(_load_file) 2. 转向量+耐久化存储(init_vector_store) 3. 查询(query), 代码全体比较简略,在进一步的细节我这儿就不解读了,仍是相对简单看理解的。

注: 特别阐明一下,咱们这儿用的抽向量的模型是Sentence-Transformer, 它是Bert的一个变种模型,Bert想必咱们是知道的。假如有不太熟悉的同学,能够转到我这边文章,来了解Bert的来龙去脉。Magic:LLM-GPT原理介绍与本地(M1)微调实战

# persist_dir = os.path.join(VECTORE_PATH, ".vectordb")
# print(persist_dir)
k2v = KnownLedge2Vector()
for s, dc in k2v.query("what is oceanbase?"):
    print(s, dc.page_content, dc.metadata)

常识查询

经过上面的步骤,咱们轻轻松松将常识转化为了向量。 那么接下来,咱们便是依据Query查询相关常识了。

咱们界说了一个KnownLedgeBaseQA, 这个类只要短短十几行代码, 所以看起来也不费力。 中心的办法就一个,get_similar_answer, 这个办法只接纳一个query字符串,依据这个query字符串,咱们就能够在咱们之前预备好的常识库傍边,查询到相关常识。

class KnownLedgeBaseQA:
    def __init__(self) -> None:
        k2v = KnownLedge2Vector()
        self.vector_store = k2v.init_vector_store()
        self.llm = VicunaLLM()
    def get_similar_answer(self, query):
        prompt = PromptTemplate(
            template=conv_qa_prompt_template,
            input_variables=["context", "question"]
        )
        retriever = self.vector_store.as_retriever(search_kwargs={"k": VECTOR_SEARCH_TOP_K})
        docs = retriever.get_relevant_documents(query=query)
        context = [d.page_content for d in docs] 
        result = prompt.format(context="\n".join(context), question=query)
        return result

推理&QA

常识都查出来了,剩余的就交给大模型吧。 咱们这儿运用的是vicuna-13b的模型,详细的示例代码如下,

是的,这儿也没什么难的,便是构造一个参数,然后发一个POST,也没啥特别好讲的。

def generate(query):
    template_name = "conv_one_shot"
    state = conv_templates[template_name].copy()
    pt = PromptTemplate(
        template=conv_qa_prompt_template,
        input_variables=["context", "question"]
    )
    result = pt.format(context="This page covers how to use the Chroma ecosystem within LangChain. It is broken into two parts: installation and setup, and then references to specific Chroma wrappers.",
              question=query)
    print(result)
    state.append_message(state.roles[0], result)
    state.append_message(state.roles[1], None)
    prompt = state.get_prompt()
    params = {
        "model": "vicuna-13b",
        "prompt": prompt,
        "temperature": 0.7,
        "max_new_tokens": 1024,
        "stop": "###"
    }
    response = requests.post(
        url=urljoin(VICUNA_MODEL_SERVER, vicuna_stream_path), data=json.dumps(params)
    )
    skip_echo_len = len(params["prompt"]) + 1 - params["prompt"].count("</s>") * 3
    for chunk in response.iter_lines(decode_unicode=False, delimiter=b"\0"):
        if chunk:
            data = json.loads(chunk.decode())
            if data["error_code"] == 0:
                output = data["text"][skip_echo_len:].strip()
                state.messages[-1][-1] = output + "▌"
                yield(output) 

最终,让咱们看看常识问答的作用吧。假如觉得作用好,为咱们点个赞吧👍

image.png

小结

综上所属,咱们讲了当时开源干流的两个扛把子强强联合的运用实战。 Vicuna-13B与Langchain在整个AI的生态里边,做的是完全不同的工作。 一个是界说框架做规范跟链接, 一个是深入中心做技能跟作用。很显然,这两条路都获得了重大的成功。 整个开展的思路,我相信很值得咱们借鉴,经过本文的介绍,期望能对咱们有一些帮助。

最终,假如你觉得本教程里边的内容对你有帮助,而且想继续重视咱们的项目,请帮忙在GitHub给咱们的项目点个赞吧❤️💗💗😊😊😊。 项目地址: github.com/csunny/DB-G…

当然了,如你开篇所见,这仅仅是咱们项目里边很小的一部分,一起这也会是一个系列教程。 假如关心咱们的项目,或许对咱们的工作感兴趣,欢迎继续重视咱们。

参阅

  • huggingface.co/
  • github.com/lm-sys/Fast…
  • python.langchain.com/en/latest/i…
  • github.com/THUDM/ChatG…
  • www.oceanbase.com/docs/oceanb…
  • github.com/UKPLab/sent…
  • arxiv.org/abs/1810.04…