为了账号安全,请及时绑定邮箱和手机立即绑定

用Langchain、Vicuna和Sentence Transformers打造问答机器人

一个开源问答机器人工具

感谢 Jon TysonUnsplash 拍摄的照片

我的关于 Langchain 和 Vicuna 的 上一篇文章 吸引了很多超出我预期的关注。因此我决定继续这个话题,进一步探索。在社区里看到有人问如何在 LLama 模型中使用嵌入技术。

我决定试一试,并分享我只用开源资源构建问答机器人的经历和感受。

要运行这个最后的示例,你需要一台性能较好的电脑。你得有一块配备至少10GB显存的GPU,或者至少32GB的内存,以便在CPU上保有模型并进行推理。

注:使用CPU的朋友们请注意,有更轻量的版本,我还没试过这些版本。

你在这篇文章里会看到什么:

  1. 如何从Vicuna或任何基于LLama的模型中提取嵌入
  2. 将文本文件中的片段提取到Chroma中
  3. 利用Sentence Transformers库提取嵌入
  4. 在简单的测试中对比Vicuna嵌入与Sentence Transformer嵌入的效果
  5. 使用我们最好的嵌入构建一个能回答关于德国问题的机器人,使用Wikitext作为事实来源。

如果你在这个练习中需要更多的代码示例,你可以使用我的代码库,在那里你可以找到这里提到的所有内容的完整源代码:https://github.com/paolorechia/learn-langchain。我计划接下来几天添加一个简单的方式安装和运行这些示例代码。

2023年1月5日更新了一下,现在支持使用流行的文本生成webui作为后台。

GitHub - paolorechia/learn-langchain基于Vicuna的AI代理 这是一个便于使用的仓库,使使用零样本或少量样本提示变得简单 github.com

这使得安装和尝试本文中的示例变得更加简单。

如何从Vicuna或其他基于LLama的模型中提取嵌入

提示:这些嵌入效果不佳,但我还是想分享一下我的经历。也许社区能找到更好的利用Llama模型嵌入的方法。

这里假设你可以本地加载Vicuna模型。如果你不行,这一步可以跳过。

Hugging Face的LLama源代码中,我们可以看到一些提取词嵌入的函数:

    class LlamaForCausalLM(LlamaPreTrainedModel):  
        def __init__(self, config):  
            super().__init__(config)  
            self.model = LlamaModel(config)  

            self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)  

            # 初始化权重并完成最终处理  
            self.post_init()  

        def get_input_embeddings(self):  
            return self.model.embed_tokens  # 设置输入嵌入

        def set_input_embeddings(self, value):  
            self.model.embed_tokens = value  # 设置输入嵌入

        def get_output_embeddings(self):  
            return self.lm_head  # 设置输出嵌入

        def set_output_embeddings(self, new_embeddings):  
            self.lm_head = new_embeddings  # 设置输出嵌入

        ...

在这里我们可以选择输入的嵌入或输出的嵌入。我一开始选用了输入的嵌入,并编写了如下的函数:

def get_embeddings(model, tokenizer, prompt):  
    input_ids = tokenizer(prompt).input_ids  
    input_embeddings = model.get_input_embeddings()  
    embeddings = input_embeddings(torch.LongTensor([input_ids]))  
    # 计算嵌入的平均值
    mean = torch.mean(embeddings[0], 0).cpu().detach()  
    # 返回平均向量
    return mean

在这里,我们实际上是将输入拆分成词元,并提取每个词元的嵌入向量。然后我把这个功能通过一个HTTP服务器提供出来,但这并不是强制性的。

这是基于 Fast API 的一个接口定义:

    @app.post("/embedding")  
    def embeddings(prompt_request: EmbeddingRequest):  
        params = {"prompt": prompt_request.prompt}  
        print("收到的提示是:", params["prompt"])  
        output = get_embeddings(model, tokenizer, params["prompt"])  
        return {"响应": [float(x) for x in output]}
从文本文件中提取一些段落到Chroma并存入其中

这部分很简单,直接用文本方式打开文件

打开 "germany.txt" 文件
内容 = 文件.read()

我用的是关于德国页面的源代码。

然后你差不多可以直接从langchain文档中复制一个例子来加载文件并将其转换为嵌入。

    from langchain.text_splitter 导入 CharacterTextSplitter  
    from langchain.vectorstores 导入 Chroma  

    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)  
    texts = text_splitter.split_text(book)  
    docsearch = Chroma.from_texts(  
        texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts)),]  
    )

然而,我们还没有词嵌入。快速查看一下 Chroma.from_texts 函数,我们发现 add_texts 函数,最终我们找到了这一段代码:

    如果 self._embedding_function 不为 None:  
        embeddings = self._embedding_function.embed_documents(list(texts))

我们可以接着来看看 OpenAIEmbeddings 类,看看这个方法是怎么写的:

    def 嵌入文档(
        self, texts: List[str], chunk_size: Optional[int] = 0
    ) -> List[List[float]]:
        """文档嵌入

        参数:
            texts: 要嵌入的文本列表。
            chunk_size: 分段大小。如果未指定分段大小,则使用类中定义的默认值。

        返回一个嵌入列表,每个文本对应一个嵌入。
        """
        # 处理大量输入文本,分批次处理
        (...)  
        # 此处省略实际代码...

还有一个功能似乎也挺相关的。

    def embed_query(self, text: str) -> List[float]:  
        """调用 OpenAI 的嵌入端点来嵌入查询文本内容。  

        参数说明:  
            text: 待嵌入的文本。  

        返回值:  
            该文本的嵌入结果。  
        """  
        # 实际代码在这里...

好的,所以我们并不关心这些函数内部的具体实现,我们只关心它们对外的接口定义。我们来为自己的嵌入类模仿这些接口的样子:

    class VicunaEmbeddings(BaseModel, Embeddings):  
        def _call(self, prompt: str) -> str:  
            p = prompt.strip()  
            print("正在发送提示:", p)  
            response = requests.post(  
                "http://127.0.0.1:8000/embedding",  
                json={  
                    "prompt": p,  
                },  
            )  
            response.raise_for_status()  
            return response.json()["response"]  

        def embed_documents(  
            self, texts: List[str], chunk_size: Optional[int] = 0  
        ) -> List[List[float]]:  
            """调用 Vicuna 服务器的嵌入端点嵌入文本。  

            Args:  
                texts: 要嵌入的文本列表。  
                chunk_size: 嵌入的块大小。如果为 None,则使用类中指定的块大小,默认为0。  

            Returns:  
                每个文本的嵌入列表。  
            """  
            results = []  
            for text in texts:  
                response = self.embed_query(text)  
                results.append(response)  
            return results  

        def embed_query(self, text) -> List[float]:  
            """调用 Vicuna 服务器的嵌入端点嵌入查询文本。  

            Args:  
                text: 要嵌入的文本。  

            Returns:  
                文本嵌入。  
            """  
            embedding = self._call(text)  
            return embedding

正如你所见,我只是让服务器来处理繁重的任务。如果你自己搞定整个服务有点棘手,可以在GitHub上看看完整的实现,作为参考。

现在,你应该能够将其粘合到一个Langchain应用中,这里有一个完整的可运行程序,导入了我们之前的类。

    from langchain_app.models.vicuna_embeddings import VicunaEmbeddings  
    from langchain.text_splitter import CharacterTextSplitter  
    from langchain.vectorstores import Chroma  

    embeddings = VicunaEmbeddings()  

    with open("germany.txt") as f:  
        book = f.read()  

    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)  
    texts = text_splitter.split_text(book)  
    docsearch = Chroma.from_texts(  
        texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))]  
    )  

    while True:  
        query = input("请输入您的搜索词: ")  
        docs = docsearch.similarity_search_with_score(query, k=1)  
        for doc in docs:  
            print(doc)
``

# 如何使用Sentence Transformers库的使用方法

Sentence-Transformers库(https://www.sbert.net/docs/installation.html)专注于构建用于相似度搜索的嵌入。它还提供了与Hugging Face的紧密集成,使其使用起来非常方便。例如,可以查看此模型卡片(https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2),以获取一个简短的例子。

所以我们需要做的就是加载模型并将一串字符串列表传递给它进行编码。就这么简单!我们怎么把它和Langchain连起来呢?在[最新版本](https://python.langchain.com/en/latest/modules/models/text_embedding/examples/sentence_transformers.html)里,它已经被整合进来了。

这使我们的代码只需两行(如下所示)(请确保已安装sentence-transformers库):

从langchain.embeddings导入SentenceTransformerEmbeddings类
embeddings = SentenceTransformerEmbeddings(model="all-MiniLM-L6-v2")

初始化SentenceTransformerEmbeddings类的实例

我们有了一个新的代码版本,这个版本使用了句子转换器嵌入。
from langchain.text_splitter import CharacterTextSplitter  
from langchain.vectorstores import Chroma  
from langchain.embeddings import SentenceTransformerEmbeddings   

embeddings = SentenceTransformerEmbeddings(model="all-MiniLM-L6-v2")  

with open("germany.txt") as f:  
    book = f.read()  

text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)  
texts = text_splitter.split_text(book)  
docsearch = Chroma.from_texts(  
    texts, embeddings, metadatas=[{"source": str(i)} for i in range(len(texts))]  
)  

while True:  
    query = input("请输入搜索内容: ")  
    docs = docsearch.similarity_search_with_score(query, k=1)  
    for doc in docs:  
        print(doc)

你应该能够在硬件有限的情况下运行这个示例。由于模型**all-MiniLM-L6-v2** 非常轻量,我们可以在CPU上直接使用它。

# 在简单测试中比较Vicuna嵌入和句子变换器

我用上述脚本测试了这两种嵌入。我们先来看我制作的Vicuna嵌入:

> 搜索内容:德国是什么?
>
> (Document(page_content=’{{clear}}\n\n=== 法 ===\n\n{{Main|德国法律|德国司法|德国执法}}’, metadata={‘source’: ‘56’}), 0.25790396332740784)

看起来不太靠谱,换个试试。

> 搜索内容:德国的气候如何?
>
> (文档(page_content=’=== 基础设施 ===\n\n{{Main|德国的交通运输|德国的能源|德国的电信|德国的供水与卫生}}\n\n[[File:ICE 3 Oberhaider-Wald-Tunnel.jpg|thumb|right|[[ICE 3]]在[[科隆—法兰克福高速铁路线]]上行驶]]’, 元数据={'source': '72'}),0.19269950687885284)

所以很遗憾的是,它们看起来与我们最初的查询完全无关。我们用句子转换模型来比较一下。

> 搜索:德国是什么?
>
> (文档内容:“德国被描述为一个[[大国]],拥有[[德国经济|强大的经济]];它是欧洲[[欧洲国家国内生产总值列表|国内生产总值最大的国家]],按名义GDP计算,是世界上[[各国国内生产总值列表|第四大经济体量]],按购买力平价计算,是[[各国国内生产总值列表|第五大经济体量]]。作为工业、[[德国科技|科学技术]]领域的全球大国,它既是世界上[[各国出口列表|第三大出口国]],也是[[各国进口列表|第三大进口国之一]]。作为一个[[发达国家]],它[[德国社会安全|提供社会安全]],[[德国医疗|提供全民医疗保健系统]],并[[德国高等教育|提供免费的大学教育]]。德国是[[联合国]]、欧盟、[[北约]]、[[欧洲理事会]]、[[G7]]、[[G20]] 和[[经合组织]]的成员。拥有[[德国的世界遗产|第三多]]的[[联合国教科文组织世界遗产]]。”,来源:4), 0.7956284284591675)

哇,好多了呢 :)

> 输入您的搜索:德国的气候如何?
>
> (文档内容(page_content='=== 气候 ===\n德国大部分地区拥有温带气候,从中北部和西部的海洋性气候到东南部的大陆性气候。冬季从阿尔卑斯山区的寒冷天气到温和的天气,通常是阴天,且降水较少,而夏季则从炎热干燥到凉爽多雨不等。北部地区盛行西风,从北海带来潮湿空气,从而使温度温和并增加降水。相反,东南部地区的温度更为极端,变化较大。<ref>{{cite web|url=https://www.britannica.com/place/Germany/Climate|website=Encyclopedia Britannica|title=Germany: Climate|accessdate=23 March 2020|archiveurl=https://web.archive.org/web/20200323124307/https://www.britannica.com/place/Germany/Climate|archivedate=23 March 2020|url-status=live}}</ref>’,元数据信息={'source': '43'}) T

所以毫不意外,**搜索功能表现**确实不错,与句子编码器配合得很好!因此,这些模型设计得非常适合这种特定用例。

你可以在这里看到更多例子。

您可能会遇到搜索效果不佳的情况,因为它只返回了一个关于主题标题的简短内容。一种可能的解决方法之一是增加检索到的文档数量。

# 我们使用我们最好的嵌入技术来构建一个可以回答关于德国的问题的机器人,使用维基文本作为信息来源。

如你在之前的例子中可能注意到的,我们返回的是未处理的wikitext,包含了很多标签,阅读起来非常困难,让人难以接受。让我们通过创建一个完整的程序来解决这个问题,让结果更易于处理,这个程序将利用这些结果和Vicuna LLM一起工作。

在我们之前的文章《使用Vicuna和Langchain创建我的第一个AI代理》(https://medium.com/creating-my-first-ai-agent-with-vicuna-and-langchain-376ed77160e3)中,我们介绍了如何设置本地的Vicuna LLM API。目前最大的挑战可能是安装所有依赖项——我将来会添加一个安装脚本来简化这个过程。假设你已经成功设置好了服务器,你可以这样运行一个量化版本:
export USE_7B_MODEL=true && export USE_4BIT=true && uvicorn servers.vicuna_server:app

一旦LLM服务器开始运行,我们就可以开始编写最终程序并执行它。

首先,我们将创建一个用于搜索我们所拥有的词嵌入的工具。
从 pydantic 导入 BaseModel, Field  # 请注意,这行实际上是一个Python导入语句,直接翻译可能不会在中文环境中执行。
class SearchInEmbeddings(BaseModel):  
    query: str = Field()  

def search(search_input: SearchInEmbeddings):  
    docs = docsearch.similarity_search_with_score(search_input, k=1)  
    返回 docs  # 注意这里的 'return' 是Python关键字,直接翻译可能不会在中文环境中执行。

tools = [  
    Tool(  
        name="Search",  
        func=search,  
        description="对于回答关于德国的问题非常有用",  
    )  
]

我们将初始化一个具有内存的代理。但是,内存在这里并非必不可少的,重要的是传递工具并使用正确的代理种类。
print("初始化VicunaLLMClient进程")  
memory = ConversationBufferMemory(memory_key="chat_history")  # 初始化会话缓存
llm = VicunaLLM()  
agent = initialize_agent(  
    tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, # 零样本反应描述代理类型
    verbose=True, # 详细输出
    memory=memory  
)

现在我们来执行最后一个循环。
while True:  
    query = input("请键入你的问题:")  
    agent.run(query)  

我们试试看
输入你的问题:德国的气候是怎样的?  

> 进入新的代理执行链...  
我应该查一下  
行动:查找  
输入内容:"德国的气候"  
观测到:[(文档(page_content='=== 气候 ===\n德国大部分地区气候温和,从北部和西部的海洋性气候到东部和东南部的大陆性气候。冬天从阿尔卑斯山南部的寒冷到一般阴冷,降水较少,夏天可以从炎热干燥到阴凉多雨。北部地区盛行西风,从北海带来湿润的空气,调节气温,增加降水。相反,东南部气温则更为极端。<ref>{{cite web|url=https://www.britannica.com/place/Germany/Climate|website=Encyclopedia Britannica|title=Germany: Climate|accessdate=23 March 2020|archiveurl=https://web.archive.org/web/20200323124307/https://www.britannica.com/place/Germany/Climate|archivedate=23 March 2020|url-status=live}}</ref>', metadata={'source': '43'}), 0.46628236770629883)]  
思考:现在我知道了德国的气候  
最终答案:德国气候温和,从北部和西部的海洋性气候到东部和东南部的大陆性气候。冬天从阿尔卑斯山南部的寒冷到一般阴冷,降水较少,夏天可以从炎热干燥到阴凉多雨。北部地区盛行西风,从北海带来湿润的空气,调节气温,增加降水。相反,东南部气温则更为极端。  

> 完成链接。


下面是更易读的最终答案格式:

> 德国具有温带气候,北部和西部为海洋性气候,而东部和东南部则为大陆性气候。冬天在南部阿尔卑斯山地区较为寒冷,而其他地区则比较凉爽,通常多云且降水较少,夏天则可能从炎热干燥变化为凉爽多雨。北部地区盛行西风,带来湿润的空气,调节气温并增加降水。相比之下,东南部的气温更加极端。

我觉得它挺不错的!

我决定尝试一个更难的例子,(完整输出链在这里:[](https://gist.github.com/paolorechia/ff8a3481094db421c33cc256997feb0e)),让我们看看效果如何。我手动从输出中提取了答案。如果你想自动化这个过程,你可以加入一个简单的解析器来提取“最终答案”这一行。

> 德国有哪些州?
>
> **德国的州是十六个组成州,统称为“联邦州”。每个州有自己的宪法,并在内部组织方面享有很大的自治权。在市政级别上,共有401个区,包括294个农村区和107个城市区。**
>
> 你能告诉我德国每个州的名字吗?
>
> **德国每个州的名字是:[[巴伐利亚州]], [[柏林]], [[勃兰登堡]], [[汉堡]], [[黑森]], [[梅克伦堡-前波美拉尼亚]], [[下萨克森]], [[北莱茵-威斯特法伦]], [[莱法州]], [[萨尔州]], [[萨克森]], [[萨克森-安哈尔特]], [[图林根]], [[不来梅]], [[爱尔福特]], [[石勒苏益格-荷尔斯泰因]], [[汉诺威]], [[莱比锡州]], [[慕尼黑]], [[纽伦]], [[斯图加特]], [[柏林]], [[莱比锡州]], [[汉诺威]]**

我们看到了两个问题:首先,它把名字放在了 `[[]]` 里——这不是什么大不了的问题,我们可以通过更多的提示或预处理等方式来解决这个问题。

第二个问题是它也将城市和州混淆了。

这是一个非常有挑战性的问题,显示了LLM在理解数据时的局限性,尤其是在数据没有预先很好地处理的情况下。

好了!希望你喜欢这篇文章哦,能对你有帮助,让你更接近目标。
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消