结对「编程/大模型」实践营作为AI雏鹰计划(AI for Young Eagle)的亮点项目,体现了AI4AI该计划的核心理念——推进人工智能的公益普及。实践营中,学员与导师「结对」,逐步深入项目开发。这种“师徒制”的教学法不仅加快了技术知识的传授,还显著提升了学员的问题解决能力。

本次 “结对大模型” 实践共 2 名学员参与,我作为导师辅助结对实践。项目主题为 “杭州大学生创业创新政策问答智能聊天助手”,目标是利用大模型技术开发一款智能聊天助手(chatbot)。该助手能够针对用户提出的相关问题,提供智能且精准的回答,并附上相应的政策依据。

在项目期间,学员们从基础框架搭建开始,在PieAIStudio平台上逐步深入学习RAG流程。通过「结对」模式,我亲自示范操作,引导学员们带着问题边学边做,完整体验项目的每个环节。在项目中,学员们尝试了大型预训练模型和LoRA微调技术,通过测试多个大模型和分段(chunking)的组合搭配,成功提升了模型的精准度和适应性。这些尝试标志着将RAG转变为一个可优化的学习过程。

一、 政策问答智能聊天助手的搭建

在构建政策问答智能聊天助手的过程中,我们采用了RAG(Retrieval-Augmented Generation)技术。RAG是一种结合了检索和生成的混合型自然语言处理技术,它通过检索相关信息来增强生成模型的上下文理解能力。RAG的主要优点在于能够有效减少生成式模型的“幻觉”问题,即模型生成与现实不符的内容,从而提高回答的准确性和可靠性。我们将整个搭建过程分为三个关键阶段:数据预处理、推理和评价。


RAG 通用框架

数据预处理

在预处理阶段,我们完成了清洗、分词并提取特征,确保数据质量。首先,我们将pdf政策文本转化成txt,这一步基于开源项目tesseract-ocr的简体中文版本实现。

def process_pages(pdf_path, start_page, end_page):
images = convert_from_path(pdf_path, dpi=300, first_page=start_page, last_page=end_page)
text_pages = {}

    for i, image in enumerate(images, start=start_page):
gray_image = ImageOps.grayscale(image)
text = pytesseract.image_to_string(gray_image, lang='chi_sim+eng')
print(f"\nPage {i} Text:\n{text}") # Print recognized text
text_pages[i] = text + "\n"

    return text_pages

在实施时,我们选取的政策文档为43页,经过jieba分词后,得到13194字,共471句话。

words = jieba.lcut(text)
num_words = len(words)

# Matches Chinese period, exclamation, question marks, and newlines
sentence_delimiters = r'[。!?]'  
sentences = re.split(sentence_delimiters, text)

sentences = [s.strip() for s in sentences if s.strip()]
num_sentences = len(sentences)

接下来是分段即chunking环节。我们调研了多种常用的分段方法,并着重实验了其中两种。

● 方法一:等字符分段法

这也是最常见的分段方法。为适应大模型每次输入token的最大数量限制,并且考虑到单句平均28个字符,我们采用 width=300,overlapping=50 的分割法。

while start < total_words:
  end = start + W
  chunk_words = words[start:end]
  chunk_text = ''.join(chunk_words) # Concatenate words without spaces
  chunks.append(chunk_text)
  start = end - overlap # Move the window forward with overlap

● 方法二:语义双重合并分段

语义双重合并分段(semantic chunking double-pass merging)中有双重过程,其中First Pass的目的是准确识别主题的差异,将最明显的句子连接在一起。而Second Pass进一步将以上小块组成主题各异的大块。对于主题的变化判定,我们设定了阈值 threshold = 0.7。
这里采用的sentence tokenizer是sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2,为句子生成84维的向量。

chunks = []
current_chunk = []
i = 0
    while i < len(sentences):
sentence = sentences[i]
is_item = bool(item_pattern.match(sentence))
        if is_item:
            # Start a new chunk for the itemized list
            if current_chunk:
chunks.append(current_chunk)
current_chunk = [sentence]
i += 1
            # Add subsequent itemized entries to the current chunk
            while i < len(sentences) and (bool(item_pattern.match(sentences[i])) or sentences[i].startswith(('(', '('))):
current_chunk.append(sentences[i])
i += 1
            # Add the completed itemized list chunk
chunks.append(current_chunk)
current_chunk = []
        else:
            # Regular sentence processing with semantic similarity
            if not current_chunk:
current_chunk = [sentence]
            else:
                # Compute similarity with the previous sentence
embedding_prev = get_sentence_embedding(current_chunk[-1])
embedding_curr = get_sentence_embedding(sentence)
sim = cosine_similarity(
embedding_prev.reshape(1, -1),
embedding_curr.reshape(1, -1)
)[0][0]
                if sim >= 0.7: # Adjust the threshold as needed
current_chunk.append(sentence)
                else:
chunks.append(current_chunk)
current_chunk = [sentence]
i += 1

    # Add any remaining chunk
    if current_chunk:
chunks.append(current_chunk)

在实践中,我们发现使用同样的大模型,第二种语义分段法的预测效果要优于第一种等字符分段法。这可能是因为在方法二的初始分段过程中,我们注意到了句块过于零散的情况:

条例化的信息在这里被视为分段的标志,而相反他们正应被归为一类。于是我们确保“()”级别的itemization都能被正确合并。

item_pattern = re.compile(r'^(\(?[一二三四五六七八九十0-9]+\)?[.。、])')

以上两种分段法得到的结果,我们都以chunks.pkl和chunk_embeddings.pkl形式存储。

推理环节

在推理环节中,我们利用大模型的深度学习功能,通过微调和优化来提高模型的理解和回答能力。我们需要依据用户提问找到相关联的文本,设计提示词,随后调用大模型作答。

先对query进行tokenization,找到相似度最高的top K段落(K=5):

def get_top_k_chunks(query_embedding, chunk_embeddings, K):
similarities = []
    for idx, chunk_embedding in enumerate(chunk_embeddings):
sim = cosine_similarity(
query_embedding.reshape(1, -1),
chunk_embedding.reshape(1, -1)
)[0][0]
similarities.append((idx, sim))
similarities.sort(key=lambda x: x[1], reverse=True)
top_k = similarities[:K]
    return top_k

为兼顾模型性能与潜在的参数优化可行性,我们选择Llama-2-7b-hf作为大模型。设计一组prompt后即可开始问答。

context = ''
    for idx, sim in top_k_chunks:
chunk_text = ''.join(chunks[idx]) if isinstance(chunks[idx], list) else chunks[idx]
context += f"【内容{idx+1}】\n{chunk_text}\n\n"
prompt = f"""你是一名智能助理,请根据以下提供的政策内容,回答用户的问题。请确保回答准确且基于提供的内容。如果无法找到答案,请告知用户。

{context}
用户提问:
{query}
你的回答:
"""
terminal>>
请输入您的问题:杭州市海外高层次人才创新创业有哪些补助?
生成的回答:
参照中国杭州大学生创业大赛在杭落地项目资助条目

可见该回答虽言之成理,仍存在改进空间。问题在于,如何量化评价这一模型的回答准确度?为此,我们引入了多项选择题(MCQ)作为评价集。

评价环节

鉴于大模型生成的自然语言回答存在不确定性,量化其准确性变得颇具挑战。为此,构建一个包含确切答案的评价集显得尤为关键。我们期望该评价集满足以下特点:

首先,每个问题应有一个正确答案,而其他三个错误答案在常识范围内应具有一定的合理性,这样可以确保判断是基于检索到的文档内容,而非模型的先验知识。

其次,正确答案应在选项中随机分布,以避免在训练过程中出现过度拟合。通过人工标注与AI技术的辅助,我们成功构建了30组评价问答题,确保了评价集的质量和实用性。

以下是示例问题:

{
        "query": "哪些企业能获得杭州市的创业补助?",
        "options": {
            "A": "所有注册在杭的企业均可申请。",
            "B": "符合政府补助要求的创新型企业。",
            "C": "补助只提供给年收入超过一定标准的企业。",
            "D": "只限于科技创新型企业。"
},
        "ground_truth": "B"
},

在这组评价集上,我们分别验证了两种分段法。鉴于生成的回答不总是如指令里声明的那样,仅仅给出ABCD中的选项,我们提取回答中首个出现的合法大写字母作为predicted answer。

for char in predicted_answer:
        if char in ['A', 'B', 'C', 'D']:
            return char
    return None

经多组实验,等字符分段法取得了13.3%-20%的准确率,而语义分段法取得了26.7%-50%的准确率。总体而言,语义分段法所产生的文本在该评价集上更加可靠,除了上述提及的合并itemization的原因,还可能是因为等字符分段法恒定的top K chunks输入宽度过大,导致大模型更难准确理解指示。此处展示正确和错误的预测案例各一组以供参考:


至此为止,我们可以对不同大模型与分段法在该评价集上的性能搭配进行总结。


不同大模型与分段方法搭配的准确率

注:显示的准确率为多组实验取得的最高值。当调用更小模型时,我们相应更改了分段的策略。例如对于microsoft/phi-2,我们选取W=80,overlap=40。对于Open_llama_7b,我们选取top K=3。

二、LoRA on RAG:从训推一体到深度学习

在学员们利用RAG技术搭建政策问答智能聊天助手的流程中,他们经历了详尽的调研和调试,已经从最初的探索者成长为能够独立处理复杂任务的专家。然而,这个项目方案仍有提升空间。

我们注意到,模型的训练和推理过程并未完全分离,导致超参数(hyperparameters)的设定过于依赖初始设计,缺乏迭代优化的过程,这是机器学习早期的常见问题。随着深度学习技术的不断进步,出现了多种调参方法。在这些方法中,LoRA因其能够在较低成本下实现大模型的局部微调而备受青睐。通过引入LoRA技术,我们可以更有效地优化模型,实现更精准的调整,从而提升整体性能。

什么是LoRA?

LoRA(Low-rank adaptation,低秩适应)是一种高效的机器学习模型微调技术,它能够迅速使模型适应新环境。与RAG专注于特定数据集不同,LoRA使模型能够更好地适应特定的任务需求。在面对多样化的细分任务时,全面微调一个大型模型往往成本过高,而LoRA提供了一种经济且快速的解决方案。通过在模型的QKV(Query, Key, Value)部分引入低秩矩阵,即形式为\(B^{m \times r} \times A^{r \times n}\)的结构,其中r远小于m和n,LoRA只需训练两个残差(residual)矩阵A和B。这种方法显著减少了训练的参数量,同时对模型的自注意力层和交叉注意力层产生影响,从而实现对模型的快速且有效的微调。

把LoRA应用到RAG Chatbot

我们先将先前的评价集dataset.json以 20:5:5 拆成 train:valid:test。设置lora_config的参数。

def fine_tune_lora(model_name, train_dataset, valid_dataset):
    # Load the pre-trained LLaMA model
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")

    # Apply LoRA to the model
lora_config = LoraConfig(
r=8, # Low-rank approximation factor
lora_alpha=16, # Scaling factor for the LoRA weights
target_modules=["q_proj", "k_proj", "v_proj"], # Target the attention layers
lora_dropout=0.1  # Dropout rate for LoRA layers
)

model = get_peft_model(model, lora_config)

把evaluation_metric设置为accuracy。定义可训练的参数,以及训练器。为节约GPU资源,可下调精度。

training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=3,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=4,
fp16=True, # Enable mixed precision
evaluation_strategy="epoch",
save_strategy="epoch",
logging_dir='./logs',
logging_steps=10,
save_total_limit=2,
load_best_model_at_end=True,
dataloader_num_workers=4,
push_to_hub=False,
metric_for_best_model="accuracy",
)

trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=valid_dataset,
tokenizer=tokenizer,
compute_metrics=compute_metrics,
)

def compute_metrics(p):
logits, labels = p
predictions = torch.argmax(logits, dim=-1)
loss = torch.nn.CrossEntropyLoss()(logits.view(-1, logits.size(-1)), labels.view(-1))
    return {
        'eval_loss': loss.item(),
        'accuracy': accuracy_score(labels.view(-1).cpu().numpy(), predictions.view(-1).cpu().numpy())
}

    # Train the model
trainer.train()

    # Save the fine-tuned LoRA model
model.save_pretrained('fine_tuned_lora_llama')
tokenizer.save_pretrained('fine_tuned_lora_llama')

最后预计需要64.00MiB GPU空间。技术原理已经阐明,限于算力资源,工程部分留作将来的拓展实践。

三、项目细节

教学计划

我们根据学员的背景和能力,设计了一系列由浅入深的项目里程碑(Project Milestones),以确保项目进度的合理性和挑战性。

● 项目里程碑 1:将指定的PDF格式政策文件转换为TXT文本,并创建大纲;基于文档内容设计10个选择题。
● 项目里程碑 2:将TXT文本分割成N个长度为W的段落(chunk)。
● 项目里程碑 3:利用现有库对给定文本进行分词(tokenize)处理。
● 项目里程碑 4*:以查询(query)为输入,计算每个段落的嵌入相似度(embedding similarity),进行排序;设计一个筛选机制(如设定阈值或累积比例),以选出相关的段落。
● 项目里程碑 5*:调用大型模型(如Llama-2-7b-hf),将查询与相关段落组合成开放式问答的输入,通过编码(encode)和解码(decode)生成回答,得到初步结果。
● 项目里程碑 6*:使用选择题集的查询进行评估,尝试不同的筛选方案并重复测试。
● 项目里程碑 7:总结项目经验,归纳学习成果,并提出改进方案。

注:每个项目里程碑默认为期一周,标有*的里程碑难度较大,可延长至两周。

项目环境

● 数据处理IDE:VSCode;
● 测试环境:拓数派旗下 PieAIStudio和 PieCloudDB;
● 训练环境:NVIDIA A100-SXM4-80GB;Driver Version: 535.183.06;CUDA Version: 12.2

参考资料

  1. GitHub - flyyuan/pdf2txt-chinese: 将影印版 PDF 图书转换为文本 TXT,供 GPTs 使用作为知识库
  2. Chunking methods in RAG: comparison - BitPeak
  3. GitHub - Lightning-AI/litgpt: 20+ high-performance LLMs with recipes to pretrain, finetune and deploy at scale.

如果你也对这些前沿技术感兴趣,欢迎扫描下方二维码,或点击链接申请入营。

作者简介

周嘉宇(Jiayu Zhou)
AI4AI志愿者/结对实践营导师

Computer Engineering,ECE Department,ZJU&UIUC。大四本科生,主要研究方向为LLM+Knowledge Graph Reasoning,有三篇在投或发表论文,立志做多模态的next-generation LLM Agent。技术栈多样,曾在证券公司数研部和数据库公司实习,并且热衷于校内外社区服务。期待与同好者合作~

学员反馈

徐子凡
上海文琦汇点美高高中生

本次「结对编程/大模型实践营」过程中,我在嘉宇老师的带领下,利用Python将图片文件成功转换为文本,并积极参与了初步训练模型的准确性测试。这个项目不仅锻炼了我的问题解决能力,还显著提高了我的编程技能。我非常荣幸能够参与AI4AI项目,并对导师和同伴们给予的帮助和支持表示衷心的感谢。

沈逸江
上海文琦汇点美高高中生

在本次项目中,我学习了数据处理和结构化查询的应用。我们用Python获取政府条例,通过AI进行结构化处理,按关键词分类。我还参与了AI模型训练,学习如何提高数据处理效率和准确性。这次项目提升了我的技术知识和实际操作能力。

致谢

本次AI公益教育项目的成功离不开各方面的支持与协助。首先,我要感谢1024Foundation发起的AI4AI倡议,为我们实践AI公益教育搭建了宝贵的平台;同时,感谢OpenPie旗下的PieAIStudio为我们提供了强大的实训平台支持。此外,我特别感谢冯雷先生(Ray Von)的言行激励,启发我积极投身公益事业;以及黄奕铖先生(Marco Huang)为我们的教学项目提供技术原型和流程指导,助力项目顺利开展并取得成效。


AI4AI社区
1 声望1 粉丝

AI for All Initiative(简称:AI4AI)项目由1024数字产业基金会发起,旨在推动人工智能(AI)能力的广泛普及和受益,通过课程、实训、活动、竞赛、等形式培养更多AI倡导者和引领者。