本项目的目标是使用RAG技术和现有LLM开发一个具有心理方面专业知识的心理行业大模型,并基于此开发一个具有日志分析总结、相关推荐、对话等功能的心理日志网站。
一、获取数据
考虑到我们项目的目标是要做一个关注大众心理、处理日常心理问题的对已有大语言模型的数据增强,需要获取心理方面的数据,我们小组下载了32.2 MB的中文心理学书籍的txt数据,筛去了故事性的书籍。
二、数据处理
在文本进行向量嵌入之前,为了保证RAG检索得到的prompt的质量,我们对书籍文本进行处理。
书籍数据常常需要一个章节甚至更多才能讲完对一个问题的分析,一个章节可能很大篇幅是问题的引入和提出,后面才是解决方案。如果直接对整本书籍切片后进行向量嵌入,怀疑检索到的数据可能是和query相关的问题引入而非一个高质量prompt需要的问题的解决方案。
文本片段与query的相似性和文本片段是否包含query的答案(相关性)是两回事。
对于这个问题,我们提出了两种处理思路:
1.使用已有的语言模型根据书籍内容生成QA对,将QA对作为RAG检索的数据源;
2.对每段需要嵌入的数据进行绑定,提取每章的主题并将其放到每段要进行嵌入文本的末尾,这样每个问题都会是主题相关;
3.两阶段检索的解决方案;
下面介绍根据这两种思路进行的实践:
在调研和尝试后发现,部分心理学书籍偏向理论性,章节都在讲述心理学的理论;有的偏向故事性,前面的引入故事较长,需要很长的篇幅才能讲述解决方案;这两种书籍都难以在检索中为RAG提供有效的prompt。考虑到RAG对数据的依赖性较强,决定放弃使用书籍作为原始数据集。
一、数据获取
检索找到两个开源的心理咨询问答数据集,格式为提出生活中的苦恼困难,下面附上回答,符合我们项目的目标。
具体数据格式如下:(这里给出的例子是一个问题对应一个回答,但问答实际上是一个列表,问题与回答是一对多的关系)
二、数据处理
因为要对数据进行嵌入,嵌入模型对文本有chunk_size的限制,且为了方便做prompt,对问答对进行绑定和切分。
下载开源的json数据集,抽取出每条问题和对应的回答,对于一个问题,根据问题和回答的长度对其进行组合。观察数据集,问题由title和content组成,一般content包含更多信息。因此如果title和content的长度小于chunk_size/3,则保留两者,否则优先选择content。一个问题和其对应的一条或多条回答组合作为一条文本写入txt文件。
这部分的具体逻辑见代码。
def extract_questions_and_answers(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
combinationList=[]
for item in data:
combination=''
question=''
residualSize=chunkSize
#question
ques_info = item['ques_info']
title = ques_info['title'].replace('\r', '').replace('\n', '')
content = ques_info['content'].replace('\r', '').replace('\n', '')
if len(content)+len(title)<chunkSize/3:
question=title+content
residualSize-=len(question)
elif len(content)<chunkSize/2:
question=content
residualSize-=len(content)
elif len(title)<chunkSize/2:
question=title
residualSize-=len(title)
else:
continue
combination=question
#answer
answers_info = item['answers_info']
i=0
while i<len(answers_info):
answerForOne=''
combination=question
count=0#记录一个当前chunk中存储了多少个answer
while i<len(answers_info):
answer=answers_info[i]
answer_content = answer['content'].replace('\r', '').replace('\n', '')
if len(answer_content)>residualSize: #单个answer大于residualSize
if count>0:#如果answerForOne已经有answer
break
else:
answerForOne+=answer_content[:residualSize-10]
i+=1
count+=1
break
elif len(answer_content)+len(answerForOne)>residualSize:#answerForOne中已有answer
break
else:
answerForOne+=answer_content
i+=1
count+=1
combination+=answerForOne
combinationList.append(combination)
return combinationList
最后获取到106417条原始文本数据。
三、文本嵌入
原本计划直接使用llamaindex进行下面的工作,但没能找到合适的llamaindex的技术文档(全面的文档只找到英文的),且如果使用openai提供的嵌入模型和LLM需要付费。因此决定先自己部署免费的嵌入模型,之后学习使用llamaindex中优化的检索策略对我们的体系进行改进。
搜索发现huggingface的m3e模型具有良好的中文文本嵌入能力,通过pip install SentenceTransformer即可直接从huggingface网站上获取模型并在本地进行向量嵌入。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('moka-ai/m3e-base').cuda()
text_documents=[]
# 打开文本文件以供读取
with open('chunk.txt', 'r', encoding='utf-8') as file:
# 逐行读取文件内容
for line in file:
# 去除行末的换行符
line = line.rstrip('\n')
text_documents.append(line)
#Sentences are encoded by calling model.encode()
embeddings = model.encode(text_documents)
注:这里使用m3e模型可能由于网络问题无法访问huggingface网站,可以更改Anaconda目录中的\Lib\site-packages\huggingface_hub目录下的constants.py文件中的ENDPOINT = os.getenv("HF_ENDPOINT") or (_HF_DEFAULT_STAGING_ENDPOINT if _staging_mode else _HF_DEFAULT_ENDPOINT)替换成ENDPOINT = " https://hf-mirror.com" ,指定使用hugging-face提供的镜像。
四、近似检索索引构建
faiss近似近邻检索库有较为全面的技术文档。我们目前的数据量为106417*784,向量条数较少而向量维度较高。决定尝试使用简单且能在一定程度上保证精度的IVFFLAT索引进行尝试,之后如果检索速度慢会调整索引。(我们的网站对用户的日志分析和建议提供并不是实时的,对RAG检索速度的要求并不是很高)
faiss官方给出的IVFFLAT实例diamagnetic如下,可以在CPU或GPU上运行索引的构建和检索。
## Using an IVF index
nlist = 100
quantizer = faiss.IndexFlatL2(d) # the other index
index_ivf = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)
# here we specify METRIC_L2, by default it performs inner-product search
# make it an IVF GPU index
gpu_index_ivf = faiss.index_cpu_to_gpu(res, 0, index_ivf)
assert not gpu_index_ivf.is_trained
gpu_index_ivf.train(xb) # add vectors to the index
assert gpu_index_ivf.is_trained
gpu_index_ivf.add(xb) # add vectors to the index
print(gpu_index_ivf.ntotal)
k = 4 # we want to see 4 nearest neighbors
D, I = gpu_index_ivf.search(xq, k) # actual search
print(I[:5]) # neighbors of the 5 first queries
print(I[-5:]) # neighbors of the 5 last queries
。。待更新