0%

Windows+anaconda+4050 6G+chatglm本地部署四

本文就文本的向量表示以及向量数据库结合大模型的使用作以介绍

中文文本向量表征

文本向量表征

这里我对比了三种不同模型embedding的结果:分别是shibing624/text2vec-base-chinesew2v-light-tencent-chinese 以及 shibing624/text2vec-base-chinese-paraphrase

调用方法如下:

1
2
3
4
from text2vec import SentenceModel, Word2Vec
model = SentenceModel("shibing624/text2vec-base-chinese")
model = Word2Vec('w2v-light-tencent-chinese')
model = SentenceModel("shibing624/text2vec-base-chinese-paraphrase")

看看不同embeddding模型对于同一输入最终表征结果的差异性:

我这里对比了三段文本

1
2
3
4
5
6
7
8
9
10
from text2vec import SentenceModel, Word2Vec
# model = SentenceModel("shibing624/text2vec-base-chinese")
# model = Word2Vec('w2v-light-tencent-chinese')
model = SentenceModel("shibing624/text2vec-base-chinese-paraphrase")
textvec1 = model.encode("铲子里还带着刚从地下带出的旧土,离奇的是,这一杯土正不停的向外渗着鲜红的液体,就像刚刚在血液里蘸过一样")
textvec2 = model.encode("“下不下去喃?要得要不得,一句话,莫七里八里的!”独眼的小伙子说:“你说你个老人家腿脚不方便,就莫下去了,我和我弟两个下去,管他什么东西,直接给他来一梭子。”")
textvec3 = model.encode("果然,这样一来他就和洞里的东西对持住了,双方都各自吃力,但是都拉不动分毫,僵持了有10几秒,就听到洞里一声盒子炮响,然后听到他爹大叫:“三伢子,快跑!!!!!!”,就觉的绳子一松,土耗子嗖一声从洞里弹了出来,好象上面还挂了什么东西!那时候老三也顾不得那么多了,他知道下面肯定出了事情了,一把接住土耗子,扭头就跑!他一口七跑出有2里多地,才敢停下来,掏出他怀里的土耗子一看,吓的大叫了一声,原来土耗子上什么都没勾,只勾着一只血淋淋的断手。他认得那手上,不由哭了出来,他手是分明是他二哥的。看样子他二哥就算不死也残废了,想到这里,他不由一咬,就想回去救他二哥和老爹,刚一回头,就看见背后蹲着个血红血红的东西,正直钩钩看着他")

print(cosine_hard(textvec1, textvec2))
print(cosine_hard(textvec1, textvec3))

由此可见,在embedding阶段选取不同的模型也会影响最后相似性的结果。一般中文比较用的最多的是shibing624/text2vec-base-chinese

一般通过在线下载方式最后模型文件夹的保存路径一般在C:\Users\xxx.cache\huggingface\hub

如果没有安装text2vec也可以通过其他两种方式加载模型完成文本embedding。

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import os
import torch
from transformers import AutoTokenizer, AutoModel

os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"


# Mean Pooling - Take attention mask into account for correct averaging
def mean_pooling(model_output, attention_mask):
token_embeddings = model_output[0] # First element of model_output contains all token embeddings
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)


# Load model from HuggingFace Hub
tokenizer = AutoTokenizer.from_pretrained('shibing624/text2vec-base-chinese')
model = AutoModel.from_pretrained('shibing624/text2vec-base-chinese')
sentences = ['如何更换花呗绑定银行卡', '花呗更改绑定银行卡']
# Tokenize sentences
encoded_input = tokenizer(sentences, padding=True, truncation=True, return_tensors='pt')

# Compute token embeddings
with torch.no_grad():
model_output = model(**encoded_input)
# Perform pooling. In this case, max pooling.
sentence_embeddings = mean_pooling(model_output, encoded_input['attention_mask'])
print("Sentence embeddings:")
print(sentence_embeddings)

方式一加载模型并将文本映射到高维空间后再次计算相似度,还是用同一文本对

和上面计算的结果稍稍有点差别。

方式二:

1
2
3
4
5
6
7
8
from sentence_transformers import SentenceTransformer

m = SentenceTransformer("shibing624/text2vec-base-chinese")
sentences = ['如何更换花呗绑定银行卡', '花呗更改绑定银行卡']

sentence_embeddings = m.encode(sentences)
print("Sentence embeddings:")
print(sentence_embeddings)

方式二的加载方式和text2vet没有差别,计算结果通过验证也没有差别。

向量相似度计算

回想之前高数的时候学过,向量之间夹角的表示

方便理解就是父母和孩子长相的相似性,亲生的就很像,这里就可以类比理解成向量的夹角Θ越接近0,孩子和陌生人就一点也不像,也就是Θ值越接近90°,就像坐标系的坐标轴,各自不能相互表示,也就是互不相关。

但是一般我们不直接求解Θ,只求到 cos Θ 就可以实现同样的效果,即 cos Θ 值越接近等于1就说明两向量越相似,cos Θ 值越接近等于0就说明两向量越不相关。

这里介绍两种计算方式

一种是直接用计算公式写的函数

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
def cos_sim_hard(v1, v2):
if not isinstance(v1, np.ndarray):
v1 = np.array(v1)
if not isinstance(v2, np.ndarray):
v2 = np.array(v2)
up = float(np.sum(v1*v2)) ## 向量乘积
down = np.linalg.norm(v1)*np.linalg.norm(v2) ## 向量模乘积
if down!=0:
res = up/down ## 计算除法一定要保证分子不为0
return res
return None

第二种是利用矩阵求解

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
def cos_sim_matrx(a, b):
if not isinstance(a, torch.Tensor):
a = torch.tensor(a) #使用cuda计算可改为torch.tensor(a).cuda(0)
if not isinstance(b, torch.Tensor):
a = torch.tensor(b) #使用cuda计算可改为torch.tensor(b).cuda(0)
if len(a.shape)==1:
a = a.unsqueeze(0)
if len(b.shape)==1:
b = b.unsqueeze(0)
a_norm = torch.nn.functional.normalize(a, p=2, dim=1)
b_norm = torch.nn.functional.normalize(b, p=2, dim=1)
return torch.mm(a_norm, b_norm.transpose(0,1))

其实第二种方法是对公式做了变式

最后看一下两种方法的计算结果:

可以看出结果是一致的。

向量数据库

接着之前的步骤,完成文本切块后,接着需要对文本向量化并存储,方便之后大模型调用

chroma向量库创建

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain.vectorstores.chroma import Chroma
from langchain.document_loaders.text import TextLoader
from langchain.embeddings.huggingface import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
loader = TextLoader('甄嬛传剧情.txt')
text_split = RecursiveCharacterTextSplitter(
chunk_size = 256,
chunk_overlap = 10,
length_function = len,
add_start_index = True)
split_docs = text_split.split_documents(loader.load())
persist_directory = 'vector_zhenhuanzhuan'
model_name = "shibing624/text2vec-base-chinese"
model_kwargs = {'device': 'cuda:0'}
encode_kwargs = {'normalize_embeddings': False}
hf = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
db = Chroma.from_documents(documents=split_docs, embedding=hf, persist_directory=persist_directory)
db.persist()

UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0xac in position 2: illegal multibyte sequence

1
2
3
loader = TextLoader('甄嬛传剧情.txt')
to
loader = TextLoader('甄嬛传剧情.txt', encoding='utf-8')

最终会在主文件夹下新建vector_zhenhuanzhuan文件夹,并保存向量文件。

搜寻近似向量

上一步已经创建好向量库,接下来测试一下输入一段文本,看能否找到最相关的文本段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 关于相似性搜索chroma提供了5种函数
'''
db.similarity_search(输入为字符串)
db.similarity_search_with_relevance_scores(输入为字符串)
db.similarity_search_with_score(输入为字符串)
db.similarity_search_by_vector(输入为字符串的embedding结果)
db.similarity_search_by_vector_with_relevance_scores(输入为字符串的embedding结果)
每种搜索结果默认返回4条文本,需要修改的话,直接按照如下指定就行
db.similarity_search(输入为字符串, K=5)
'''
ques = '甄嬛离宫去了哪儿?'
ques_embedding = hf.embed_query(ques) #这里直接调用前文定义的embedding模型

res_similarity_search = db.similarity_search(ques)
res_similarity_search_with_relevance_scores = db.similarity_search_with_relevance_scores(ques)
res_similarity_search_with_score = db.similarity_search_with_score(ques)
res_similarity_search_by_vector = db.similarity_search_by_vector(ques_embedding)
res_similarity_search_by_vector_with_relevance_scores = db.similarity_search_by_vector_with_relevance_scores(ques_embedding)

可以看出不同搜寻相似向量的返回结果都是殊途同归的,其实看源码的话,会发现有些方法其实是套壳写的,比如similarity_search里面就是调用了similarity_search_with_score

再看看向量输入与字符串输入的搜寻近似结果

similarity_search_with_score 与 similarity_search_by_vector_with_relevance_scores 的结果一模一样,完全相等

至此流程跑通了,现在重点看看搜索结果的匹配度,这块直接影响到后期大模型回答问题的准确性,所以需要调整一下

开始我设置切块文本长度在256,想了一下,这里原始数据来自于高度提炼和总结的文本,所以是不是应该把断句调小一些,这样更准呢?所以我把256换成64,重新创建新的数据库,再搜索输入看看返回的准确性。

屏幕截图 2024-03-22 163644

效果真的好了很多,很多问题都能找到正确答案,但是也要注意:1. 问题问的太细其实是匹配不到结果的,也对应了原始数据中没有匹配的数据;2. 提问可以多样性,也能找到答案

向量库+大模型

思路是:调用大模型,根据输入问题在向量库里搜寻最相似的文档段集合并返回,由llm归纳输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
retrieval_ = db.as_retriever()
from langchain.prompts import PromptTemplate
QA_CHAIN_PROMPT = PromptTemplate.from_template("""
如果你不知道答案,就回答不知道,不要试图编造答案。
总是在答案结束时说”谢谢你的提问!“
{context}
问题:{question}
""")
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever = retrieval_,
verbose=True,
chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

response = qa.run("翠果打谁了")
print(response)

最终看看大模型的归纳结果如何

屏幕截图 2024-03-22 165411

屏幕截图 2024-03-22 165429

屏幕截图 2024-03-22 165449

根据官网说明,提供了3种向量数据库,

不同的数据库需要先完成依赖安装才能使用。

遇到小插曲

在Windows上遇到了“Symbol cudaLaunchKernel not found,…,RuntimeError: Library cublasLt is not initialized”

网上的方法对我没有用,我在用nvcc -V检查cuda的时候提示nvcc命令无效,应该是cuda出现了问题。所以重新安装了cuda,还是用的之前的版本。重新安装后再运行代码文件就也没再报错了。

参考链接

Text2vec: Text to Vector

text2vec-base-chinese

FAISS和Chroma:两种流行的向量数据库的比较