Common Knowledge

本文汇总了一系列算法技巧和数学方法,包括四面体的计算、集合的枚举、头文件便利贴、点旋转坐标公式等,并介绍了卡特兰数、默慈金数等数列的应用。

2014-08-23 11:01:11

-6:四面体

(1)内切球半径:r = 3V / (S1+S2+S3+S4)

(2)体积:将四点组成三个向量AB,AC,AD,向量的混合积就是它们组成的平行六面体的体积,四面体体积是其体积的1/6。

-5:枚举一个集合(设为s)的所有超集,总共N个物品。(ts为集间差)

for(int f = s; f < (1 << N); f = (f + 1) | s){
    int ts = s ^ f;
        
}

-4:枚举一个集合(设为s)的所有非空子集,总共N个物品。(ts为集间差),注意空集需要另外讨论。

for (int f = s; f > 0; f = (f - 1) & s) {
    int ts = s ^ f;

}

-2:头文件便利贴(C++11)

#include <bits/stdc++.h>
using namespace std;

#define getmid(l,r) ((l) + ((r) - (l)) / 2)
#define MEM(a,b) memset(a,b,sizeof(a))
#define MP(a,b) make_pair(a,b)
#define PB push_back

typedef long long ll;
typedef pair<int,int> pii;
const double eps = 1e-8;
const int INF = (1 << 30) - 1;
const int MAXN = 100010;

int main(){

    
    return 0;
}

-3:头文件便利贴

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <vector>
#include <map>
#include <set>
#include <stack>
#include <queue>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;

#define getmid(l,r) ((l) + ((r) - (l)) / 2)
#define MEM(a,b) memset(a,b,sizeof(a))
#define MP(a,b) make_pair(a,b)
#define PB push_back

typedef long long ll;
typedef pair<int,int> pii;
const double eps = 1e-8;
const int INF = (1 << 30) - 1;

-1:vim配置文件

set nu ci si ai mouse=a ts=4 sts=4 sw=4 cursorline

 

0:数论

  (1)计算比m小,且与m互质的的正整数的和:phi(m)*m/2

  (2)计算比m小,且与m不互质的正整数的和:(phi(m)+1)*m/2

  (3)同余符号: ≡

  (4)1和任何数都成倍数关系,但和任何数都互质。

1:运算符优先级

2:素数定理

3:反素数表

1,2,4,6,12,24,36,48,60,120,180,240,360,720,840,1260,1680,2520,5040,7560,10080,15120,20160,25200,27720,45360,50400,55440,83160,110880,166320,221760,277200,332640,498960,554400

4:点旋转坐标公式:

 任意点(x,y),绕一个坐标点(rx0,ry0)逆时针旋转a角度后的新的坐标设为(x0, y0),有公式:

    x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;

    y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;

5:快速读入:

inline int Read(){
      int x = 0,f = 1;char ch = getchar();
      while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
      while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
      return x * f;
}

6:关闭C++读入同步

ios::sync_with_stdio(false);

7:快速输出:

void Write(int v){
      if(v>9) Write(v/10);
      putchar(v%10+'0');
}

8:正无穷

(1)int型边界:const int INF = ~0U >> 1; 

9:简洁的组合数打表(from wjmzbmr): 

for (int i = 0; i < 500; ++i) {
     for (int j = 0; j <= i; ++j) {
          comb[i][j] =
                 (i == 0 || j == 0) ?
                         1 : (comb[i - 1][j] + comb[i - 1][j - 1]);
     }
}

 10:循环版求gcd(from tourist)

int gcd(int a, int b) {
  while (a > 0 && b > 0)
    if (a > b) a %= b;
    else b %= a;
  return a + b;
}

 11:atoi 和 itoa 函数  参考:博文

  ● atoi():将字符串转换为整型值。
  ● atol():将字符串转换为长整型值。

  ● itoa():将整型值转换为字符串。
  ● ltoa():将长整型值转换为字符串。

 12:卡哈希

  参考codeforces博文

  参考bzoj题目

13:unique函数的一些注意点:

  数组下标从0开始:int sz = unique(a,a + n) - a;

  数组下标从1开始:int sz = unique(a + 1,a + n + 1) - a - 1;

14:生成 long long 范围 内的随机数:(不确定是否正确)

val = ((ll)rand() << 32) + ((ll)rand() << 1) + 1;

 15:快速预处理阶乘逆元

  先求出 (MAXN - 1)! 的逆元,然后倒序依次求出:(MAXN - 2)! ~ 1! 的逆元

  注意:如果MAXN - 1 >= mod,那么不能直接这么用。因为 fac[mod] % mod = 0

int fac[MAXN],afac[MAXN];

int Q_pow(int x,int y){
    int res = 1;
    while(y){
        if(y & 1) res = 1ll * res * x % mod;
        x = 1ll * x * x % mod;
        y >>= 1;
    }
    return res % mod; //小心模数为1的情况
}

void Pre(){
    fac[0] = afac[0] = 1;
    for(int i = 1; i < MAXN; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
    afac[MAXN - 1] = Q_pow(fac[MAXN - 1],mod - 2);
    for(int i = MAXN - 1; i > 1; --i) afac[i - 1] = 1ll * afac[i] * i % mod;
}

 16:内联汇编快速乘法

inline ll mulmod(ll x, ll y, ll mod)
{
    ll ret = 0;
    __asm__("movq %1,%%rax\n imulq %2\n idivq %3\n":"=d"(ret):"m"(x),"m"(y),"m"(mod):"%rax");
    return ret;
}

 17:扩栈命令

#pragma comment(linker, "/STACK:102400000,102400000")

 18:fread 快速读入黑科技

const int BUFSIZE=120<<20; //根据题目数据而定
char Buf[BUFSIZE+1],*buf=Buf;
template<class T>
inline void scan(T&a){
  for (a=0;*buf<'0'||*buf>'9';buf++);
  while (*buf>='0'&&*buf<='9'){a=a*10+(*buf-'0');buf++; }
}

fread(Buf,1,BUFSIZE,stdin); //加到main函数第一行

 19:根据日期算星期(基姆拉尔森公式),1~6对应星期一~星期六,0对应星期日

1 int Cal(int d,int m,int y){
2     if(m <= 2) m += 12,y--;
3     return (d + 1 + 2 * m + 3 * (m + 1) / 5 + y + y / 4 - y / 100 + y / 400) % 7;
4 }

 20:卡特兰数

数列:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796

h(n)=h(n-1)*(4*n-2)/(n+1);

h(n)=C(2n,n)/(n+1) (n=0,1,2,...)

21:默慈金数

数列:1, 2, 4, 9, 21, 51, 127, 323, 835, 2188, 5798

 与卡特兰数的联系:

   

转载于:https://www.cnblogs.com/naturepengchen/p/3930784.html

import os import requests import hnswlib import numpy as np from typing import List, Tuple import re import json #TODO: 添加prompt,确保回答被{}包裹 #TODO:加分项,修改prompt,思考怎么样LLM更容易正确回答 naive_prompt = """ ----Role---- You are a helpful language model (LLM) tasked with using texts and any necessary common knowledge to answer a given question. """ class LocalSentenceSplitter: def __init__(self, chunk_size, chunk_overlap) -> None: self.chunk_size = chunk_size self.chunk_overlap = chunk_overlap @staticmethod def _zng(paragraph): pattern = u'([!?。…!?.\n])' return [sent for sent in re.split(pattern, paragraph, flags=re.U)] @staticmethod def _zng_new(paragraph): sentence = [] #TODO: 添加split方法 # 使用自己的方法来分割句子 return sentence def split_text(self, segment): chunks, chunk_now, size_now = [], [], 0 no_left = False for s in LocalSentenceSplitter._zng_new(segment): #TODO: 添加chunk_now的判断,如果chunk_now的长度大于chunk_size,则进行分割 # 额外需要确保正确overlap return chunks def _get_overlap(self, chunk): rchunk = chunk[:] rchunk.reverse() size_now, overlap = 0, [] for s in rchunk[:-1]: overlap.append(s) size_now += len(s) if size_now > self.chunk_overlap: break overlap.reverse() return overlap, size_now class OllamaEmbedding: """ 调用本地 Ollama 的 /embeddings 接口来获取文本的Embedding。 假设 Ollama 版本支持 embeddings 功能,且在 localhost:11434 端口提供服务。 你可以根据需要修改 URL、超时、模型名称等。 """ def __init__(self, host="http://localhost:11434", model="bge-large", work_dir="work_dir"): self.host = host self.model = model self.work_dir = work_dir # 日志文件,可在此记录一些embedding的调用信息 self.log_path = os.path.join(self.work_dir, "logs.txt") os.makedirs(self.work_dir, exist_ok=True) def _log(self, msg: str): with open(self.log_path, "a", encoding="utf-8") as f: f.write(msg + "\n") def encode(self, texts: List[str]) -> np.ndarray: """ 输入若干文本,返回它们的向量(shape=[len(texts), dim])。 """ embeddings = [] for text in texts: try: payload = { "model": self.model, "prompt": text } # Ollama 的 /embeddings 接口(注意版本不同,接口可能略有差异) url = f"{self.host}/api/embeddings" r = requests.post(url, json=payload, timeout=60) r.raise_for_status() data = r.json() # data 应该包含 "embedding" 字段,这里做个简单取值 emb = data.get("embedding", []) embeddings.append(emb) self._log(f"[encode] success for text: {text[:30]}..., embedding dim={len(emb)}") except Exception as e: # 异常时可记录日志,返回一个零向量或者直接抛出 self._log(f"[encode] error for text: {text[:30]}..., error={e}") # 这里简单用全0向量顶替 embeddings.append([0.0]*1024) return np.array(embeddings) class HNSWVectorStore: """ 用 hnswlib 来管理向量索引。采用 'cosine' 作为距离度量。 """ def __init__(self, embedding_dim: int, work_dir: str = "work_dir"): self.embedding_dim = embedding_dim self.work_dir = work_dir os.makedirs(self.work_dir, exist_ok=True) # index.bin是hnswlib的索引文件,labels.npy是文本的label self.index_path = os.path.join(self.work_dir, "index.bin") self.labels_path = os.path.join(self.work_dir, "labels.npy") # 判断索引文件是否已经存在,决定是加载还是新建 if os.path.exists(self.index_path) and os.path.exists(self.labels_path): #TODO: 添加加载方法 print("[HNSWVectorStore] index loaded from disk.") else: #TODO: 添加初始化方法 print("[HNSWVectorStore] created new empty index.") def add_texts(self, texts: List[str], embeddings: np.ndarray): """ 将文本和其向量插入 HNSW 索引,并记录 labels(即文本内容)。 """ num_new = embeddings.shape[0] start_id = len(self.labels) ids = np.arange(start_id, start_id + num_new) # 插入向量 self.index.add_items(embeddings, ids) # 记录文本,以便检索时能得到对应的文本 self.labels.extend(texts) def save(self): """ 将索引和对应 labels 保存到磁盘。 """ self.index.save_index(self.index_path) np.save(self.labels_path, np.array(self.labels, dtype=object), allow_pickle=True) print("[HNSWVectorStore] index saved.") def similarity_search(self, query_embedding: np.ndarray, top_k: int = 3) -> List[str]: """ 基于向量检索最相似的 Top-K 文本。 """ # hnswlib 返回的是 (ids, distances) ids, distances = self.index.knn_query(query_embedding, k=top_k) # ids shape: (1, top_k) distances shape: (1, top_k) # 这里假设只有一个查询向量,所以取 ids[0] print(ids) result_texts = [self.labels[idx] for idx in ids[0]] return result_texts class NaiveRAG: """ 一个简易的RAG示例:用本地 Ollama 生成Embedding,再用HNSW检索相似文本,最后拼接回答。 """ def __init__(self, embedding_dim: int = 1024, work_dir="work_dir", model="bge-large"): self.work_dir = work_dir self.embedding_model = OllamaEmbedding( host="http://localhost:11434", model=model, work_dir=work_dir ) self.vector_store = HNSWVectorStore(embedding_dim=embedding_dim, work_dir=work_dir) def build_index(self, text_chunks: List[str]): """ 对text_chunks做embedding并插入索引,然后保存索引到磁盘。 """ embeddings = self.embedding_model.encode(text_chunks) # [N, dim] self.vector_store.add_texts(text_chunks, embeddings) self.vector_store.save() def generate_prompt(self, query: str, top_k: int = 3) -> str: """ 对用户query做embedding,然后从索引中检索top_k文本块,用简单方式拼接。 """ query_emb = self.embedding_model.encode([query])[0] # shape=[dim] retrieved_texts = self.vector_store.similarity_search(query_emb, top_k=top_k) #TODO:加分项添加reranker,对retrieved_texts进行rerank #print(retrieved_texts) # 这里仅用一个很简单的拼接回答示例 context = "\n".join(retrieved_texts) #answer = ( # f"You are a helpful assistant please answer the question {query} with given context:\n{context}" #) prompt = naive_prompt+f"Q:{query}\ntexts:{context}\nA:" return prompt 请修改以上代码,完成撰写 LocalSentenceSplitter 对语料进行分段,调整LocalSentenceSplitter探究分块机制对问答效果影响;利用 Embedding模型 ,完善 HNSWVectorStore构建向量索引,
06-26
<think>我们首先完成LocalSentenceSplitter类,按照要求实现文本分块。分块要求:将文本分割成句子,然后合并句子直到达到chunk_size(以字符数计算),并允许chunk_overlap(重叠部分)。具体步骤:1.使用_zng_new方法分割句子(这里我们先实现一个简单的分割方法,按标点分割,但原代码中_zng方法已经做了这个,我们可以直接使用_zng,但要求用_zng_new,所以我们可以将_zng_new实现为和_zng一样,或者改进。但题目要求TODO是添加split方法,所以我们可以先按原_zng的方式分割,然后改进为更合适的中文分割?)注意:原_zng方法使用正则表达式分割,但会保留分隔符。我们这里希望分割后的句子是完整的(包括标点),所以可以在分割后将标点加回去。2.在split_text方法中,遍历分割后的句子,将句子加入当前块(chunk_now),并累计字符数(size_now)。当当前块的大小超过chunk_size时,就将当前块(除去最后几个句子以达到重叠)作为一个块,然后新块从重叠部分开始。3.处理重叠:我们定义一个_get_overlap方法,从当前块的末尾开始取句子,直到取到的句子总长度超过chunk_overlap(但不超过),然后这些句子作为下一个块的开头。4.注意:最后一个块的处理。由于原代码中split_text方法有TODO,我们来实现它。另外,我们调整一下,让分割后的句子包括标点。原_zng方法分割后,标点作为单独的元素,我们可以将句子和后面的标点合并。但是,原_zng_new方法要求使用自己的分割方法。我们可以这样设计_zng_new:-先按原_zng方法分割,得到列表,然后合并:将连续的两个元素(句子和标点)合并成一个完整的句子。-例如:分割["今天天气很好","。","我们去公园","!"]->合并为["今天天气很好。","我们去公园!"]因此,我们实现_zng_new如下:然后,我们完成split_text方法:步骤:初始化:chunks(结果列表),chunk_now(当前块句子列表),size_now(当前块字符数)遍历每个句子(由_zng_new分割得到):如果当前块为空,直接加入句子,并更新size_now。否则,如果加入当前句子后size_now+len(sentence)<=chunk_size,则加入并更新size_now。否则,说明当前块已满,需要切割:先计算当前块的重叠部分(调用_get_overlap),然后将当前块(去掉重叠部分)作为一个块添加到chunks。然后,新块从重叠部分开始,并且将当前句子加入新块(注意:当前句子可能很长,但也要加入,因为即使超过也要保证每个句子是完整的)然后重置size_now为重叠部分的长度+当前句子的长度。注意:最后一个块在循环结束后要加入。但是,重叠部分可能不够,或者当前句子特别长,超过chunk_size,我们也要保证整个句子被加入,所以不能分割句子。因此,我们修改:如果当前块非空,且加入当前句子后超过chunk_size,那么:-如果当前块不为空,那么将当前块(包括重叠部分)作为一个块,然后新块从重叠部分开始,并加入当前句子。-如果当前块为空(比如第一个句子就超过chunk_size),那么直接将整个句子作为一个块,然后重置当前块为空,size_now=0,并继续(但这样这个块就没有重叠了?)但这样处理:因为一个句子超过chunk_size,我们不可能分割,所以整个句子作为一个块。然后下一个块从空开始。但是,题目要求探究分块机制对问答效果影响,所以我们需要确保分块合理。我们重新设计split_text:初始化:chunks=[]#存储所有块current_chunk=[]#当前块的句子列表current_length=0#当前块的总字符数#另外,我们需要一个缓冲区,用于存储可能的重叠部分?不,我们最后用_get_overlap来取重叠。遍历每个句子s(注意:每个句子是一个字符串):如果current_length+len(s)<=self.chunk_size:将s加入current_chunk,current_length+=len(s)否则:如果current_chunk不为空:将current_chunk作为一个块(先不急着加,因为可能后面还有重叠要求)但这里我们直接切割:先得到这个块,然后计算它的重叠部分(用于下一个块的开头)将current_chunk(不包括重叠部分)作为一个块,加入chunks然后,current_chunk=重叠部分的句子列表(注意:重叠部分也是句子)current_length=重叠部分的总字符数然后,将当前句子s加入current_chunk,current_length+=len(s)#注意:这里可能current_length已经超过chunk_size,但没关系,因为下一个块会继续切割如果current_chunk为空(即第一个句子就超长):将s单独作为一个块(因为不可能分割句子),加入chunks然后重置current_chunk为空,current_length=0#注意:这样这个长句子作为一个块,然后下一个句子重新开始,没有重叠循环结束后,如果current_chunk不为空,则将其加入chunks。但是,这样可能导致一个长句子被作为一个块,然后下一个块从空开始,没有重叠。这符合要求吗?另外,重叠部分怎么取?我们在切割当前块时,调用_get_overlap(current_chunk)得到重叠部分(句子列表)和重叠部分的总字符数。然后,新的current_chunk就设置为重叠部分,current_length设置为重叠部分的总字符数。注意:切割当前块时,当前块是完整的(包括多个句子),我们取它的最后几个句子作为重叠,重叠长度不超过chunk_overlap。具体实现:我们实现_get_overlap方法:输入:chunk(一个句子列表)输出:overlap(句子列表),以及overlap的总字符数从chunk的最后一个句子开始往前取,直到取到的句子的总字符数超过chunk_overlap(注意:我们取尽可能多的句子,但总字符数不超过chunk_overlap?但要求是超过chunk_overlap就停止?题目要求重叠不超过chunk_overlap,所以我们应该取到刚好超过就停止,然后去掉最后一个句子?或者取到不超过的最大值?)我们这样:反向遍历chunk(除了第一个句子,因为第一个句子如果被包含在重叠里,那么下一个块就会重复这个句子,但重叠部分应该是从后面开始取):初始化一个临时列表overlap_sentences,临时长度temp_len=0从最后一个句子开始(倒序遍历):如果加上这个句子后,temp_len+len(sentence)<=chunk_overlap或者overlap_sentences为空(即至少取一个句子):将这个句子加入overlap_sentences(插入到开头,因为我们是倒序的)temp_len+=len(sentence)否则:停止然后返回overlap_sentences(顺序是正常的,因为我们插入到开头)和temp_len但是,我们可能取不到任何句子?不会,因为至少可以取最后一个句子(因为chunk非空,且最后一个句子长度可能为0?一般不会)。所以至少有一个句子。但是,注意:重叠部分不能取整个块,因为这样就没有切割了。所以,重叠部分最多取到chunk_overlap,而且是从块的末尾开始取。因此,我们实现split_text如下:接下来,我们完善HNSWVectorStore的加载和初始化部分。在__init__中:如果索引文件存在,则加载:self.index=hnswlib.Index(...)self.index.load_index(self.index_path)#注意:hnswlib加载索引后,还需要设置ef参数(在搜索时设置)?另外,我们还需要知道构建时的参数(如空间类型,维度等),我们在保存索引的时候并没有保存这些参数,所以我们在构建索引时需要记录?或者,hnswlib的load_index会恢复这些参数?根据文档,load_index会恢复索引的所有状态(包括空间类型、维度等)。所以我们可以直接load。然后,加载labels:self.labels=list(np.load(self.labels_path,allow_pickle=True))#注意:保存时是np.array,加载后转为list如果不存在,则新建:self.index=hnswlib.Index(space='cosine',dim=embedding_dim)#初始化索引:指定最大元素数量(可以先设置一个估计值,比如1000,后续添加时如果不够会自动扩展?但hnswlib在初始化时需要指定max_elements,后续可以调整,但最好一开始就设置一个足够大的数)#我们这里先设置max_elements=0,然后在add_texts时再调整大小?但是,hnswlib要求初始化时必须指定max_elements,且大于0。我们可以先设置为10000,后面如果不够再调整(使用resize_index)self.index.init_index(max_elements=10000,ef_construction=200,M=16)self.labels=[]#初始为空列表另外,在add_texts时,我们需要检查当前索引的最大容量是否够用,如果不够,需要扩容:current_max=self.index.get_max_elements()new_count=len(texts)ifcurrent_max<len(self.labels)+new_count:#需要扩容,扩容到(len(self.labels)+new_count)*1.5new_size=int((len(self.labels)+new_count)*1.5)self.index.resize_index(new_size)然后,我们调用add_items将向量加入索引。最后,在save时,保存索引和labels。注意:hnswlib的索引保存为文件,labels保存为npy文件。接下来,我们修改NaiveRAG的generate_prompt方法,使其返回的prompt格式符合要求(用{}包裹?但题目要求添加prompt确保回答被{}包裹,这个在naive_prompt里没有体现,所以我们修改naive_prompt?)题目要求:添加prompt,确保回答被{}包裹。所以我们在naive_prompt中加入要求,例如:在最后加上:请注意,你的回答必须用大括号{}包裹起来。但是,我们也可以修改拼接方式,在问题后面加上这个要求。例如:prompt=naive_prompt+f"Q:{query}\ntexts:{context}\nA:请将最终答案用大括号{{}}包裹起来,例如{{答案}}。"但是,这样可能会影响模型回答。我们也可以要求模型以特定格式回答,在naive_prompt中说明。所以,我们修改naive_prompt:naive_prompt="""----Role----Youareahelpfullanguagemodel(LLM)taskedwithusingtextsandanynecessarycommonknowledgetoansweragivenquestion.Pleasewrapyourfinalanswerwithcurlybraces,likethis:{answer}."""这样,模型就会用大括号包裹答案。但是,我们也要考虑到,如果模型不遵守怎么办?我们可以通过后续解析来提取大括号内的内容,但这里不是重点。最后,我们按照要求完成代码,并添加必要的注释。由于代码较长,我们只修改和完成有TODO的部分。注意:我们假设OllamaEmbedding模型返回的向量维度为1024(默认),所以HNSWVectorStore初始化时embedding_dim=1024。下面,我们开始修改代码。</think>我将为您完善代码,实现文本分块和向量索引功能。以下是修改后的代码: ```python import os import requests import hnswlib import numpy as np from typing import List, Tuple, Optional import re import json import logging # 设置日志 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # 修改后的提示模板,要求回答用{}包裹 naive_prompt = """ ----Role---- You are a helpful language model (LLM) tasked with using texts and any necessary common knowledge to answer a given question. Please wrap your final answer in curly braces like this: {{answer}}. """ class LocalSentenceSplitter: def __init__(self, chunk_size: int = 500, chunk_overlap: int = 100) -> None: if chunk_size <= chunk_overlap: raise ValueError("chunk_size must be larger than chunk_overlap") self.chunk_size = chunk_size self.chunk_overlap = chunk_overlap logger.info(f"Initialized splitter with chunk_size={chunk_size}, chunk_overlap={chunk_overlap}") @staticmethod def _zng(paragraph: str) -> List[str]: """使用正则分割句子(保留标点)""" pattern = u'([!?。…!?.\n])' segments = re.split(pattern, paragraph, flags=re.U) # 合并句子和标点 sentences = [] for i in range(0, len(segments)-1, 2): if i+1 < len(segments): sentences.append(segments[i] + segments[i+1]) else: sentences.append(segments[i]) return [s for s in sentences if s.strip()] def split_text(self, text: str) -> List[str]: """分割文本为chunk_size大小的块,允许chunk_overlap重叠""" sentences = self._zng(text) chunks = [] current_chunk = [] current_length = 0 for sentence in sentences: sent_len = len(sentence) # 如果句子本身大于chunk_size,单独处理 if sent_len > self.chunk_size: if current_chunk: chunks.append("".join(current_chunk)) current_chunk = [] current_length = 0 # 将长句子分割为多个chunk for i in range(0, sent_len, self.chunk_size - self.chunk_overlap): chunk = sentence[i:i + self.chunk_size] chunks.append(chunk) # 设置重叠部分 current_chunk = [chunk[-self.chunk_overlap:]] if self.chunk_overlap > 0 else [] current_length = len(current_chunk[0]) if current_chunk else 0 continue # 检查添加句子后是否超过chunk_size if current_length + sent_len > self.chunk_size: # 完成当前chunk chunks.append("".join(current_chunk)) # 创建重叠部分作为新chunk的开头 overlap_text = "".join(current_chunk[-self.chunk_overlap:]) current_chunk = [overlap_text] if self.chunk_overlap > 0 else [] current_length = len(overlap_text) # 添加句子到当前chunk current_chunk.append(sentence) current_length += sent_len # 添加最后一个chunk if current_chunk: chunks.append("".join(current_chunk)) logger.info(f"Split text into {len(chunks)} chunks") return chunks class OllamaEmbedding: """调用本地Ollama的/embeddings接口获取文本嵌入向量""" def __init__(self, host: str = "http://localhost:11434", model: str = "bge-large", work_dir: str = "work_dir", timeout: int = 120): self.host = host self.model = model self.work_dir = work_dir self.timeout = timeout self.dimension = 1024 # 默认维度 os.makedirs(self.work_dir, exist_ok=True) self.log_path = os.path.join(self.work_dir, "embedding_logs.txt") # 测试连接并获取维度 self._validate_connection() def _log(self, msg: str): with open(self.log_path, "a", encoding="utf-8") as f: f.write(f"{msg}\n") def _validate_connection(self): """验证Ollama连接并获取嵌入维度""" try: test_text = "connection test" payload = {"model": self.model, "prompt": test_text} response = requests.post( f"{self.host}/api/embeddings", json=payload, timeout=10 ) response.raise_for_status() data = response.json() self.dimension = len(data.get("embedding", [0]*1024)) logger.info(f"Connected to Ollama. Embedding dimension: {self.dimension}") except Exception as e: logger.error(f"Ollama connection failed: {str(e)}") raise ConnectionError("Failed to connect to Ollama embedding service") def encode(self, texts: List[str]) -> np.ndarray: """将文本列表转换为嵌入向量矩阵""" embeddings = [] for text in texts: try: payload = {"model": self.model, "prompt": text} response = requests.post( f"{self.host}/api/embeddings", json=payload, timeout=self.timeout ) response.raise_for_status() data = response.json() embedding = data.get("embedding", [0.0]*self.dimension) embeddings.append(embedding) self._log(f"Embedding generated for text: {text[:50]}...") except Exception as e: logger.error(f"Embedding failed for text: {text[:50]}... Error: {str(e)}") embeddings.append([0.0]*self.dimension) return np.array(embeddings, dtype=np.float32) class HNSWVectorStore: """使用hnswlib管理向量索引(余弦相似度)""" def __init__(self, embedding_dim: int, work_dir: str = "work_dir", max_elements: int = 10000, ef_construction: int = 200, M: int = 16): self.embedding_dim = embedding_dim self.work_dir = work_dir os.makedirs(self.work_dir, exist_ok=True) self.index_path = os.path.join(self.work_dir, "index.bin") self.labels_path = os.path.join(self.work_dir, "labels.npy") self.metadata_path = os.path.join(self.work_dir, "metadata.json") # 加载现有索引或创建新索引 if os.path.exists(self.index_path) and os.path.exists(self.labels_path): self._load_index() logger.info("Vector store loaded from disk") else: self._create_new_index(max_elements, ef_construction, M) logger.info("Created new vector store") def _load_index(self): """从磁盘加载现有索引""" # 加载标签 self.labels = list(np.load(self.labels_path, allow_pickle=True)) # 加载元数据 if os.path.exists(self.metadata_path): with open(self.metadata_path, 'r') as f: metadata = json.load(f) self.space = metadata['space'] self.M = metadata['M'] self.ef_construction = metadata['ef_construction'] else: # 默认值 self.space = 'cosine' self.M = 16 self.ef_construction = 200 # 初始化索引 self.index = hnswlib.Index(space=self.space, dim=self.embedding_dim) self.index.load_index(self.index_path) # 设置索引参数 self.index.set_ef(100) # 设置搜索时的ef参数 self.index.set_num_threads(4) def _create_new_index(self, max_elements: int, ef_construction: int, M: int): """创建新索引""" self.space = 'cosine' self.M = M self.ef_construction = ef_construction self.labels = [] # 初始化索引 self.index = hnswlib.Index(space=self.space, dim=self.embedding_dim) self.index.init_index( max_elements=max_elements, ef_construction=ef_construction, M=M ) self.index.set_ef(100) self.index.set_num_threads(4) # 保存元数据 metadata = { 'space': self.space, 'M': M, 'ef_construction': ef_construction } with open(self.metadata_path, 'w') as f: json.dump(metadata, f) def add_texts(self, texts: List[str], embeddings: np.ndarray): """添加文本及其嵌入向量到索引""" if len(texts) != embeddings.shape[0]: raise ValueError("Texts and embeddings count mismatch") current_count = len(self.labels) new_ids = np.arange(current_count, current_count + len(texts)) # 确保索引容量足够 if current_count + len(texts) > self.index.get_max_elements(): new_size = int((current_count + len(texts)) * 1.5) logger.info(f"Resizing index from {self.index.get_max_elements()} to {new_size}") self.index.resize_index(new_size) # 添加向量到索引 self.index.add_items(embeddings, new_ids) # 添加文本标签 self.labels.extend(texts) logger.info(f"Added {len(texts)} texts to vector store") def save(self): """保存索引到磁盘""" self.index.save_index(self.index_path) np.save(self.labels_path, np.array(self.labels, dtype=object), allow_pickle=True) logger.info(f"Vector store saved with {len(self.labels)} items") def similarity_search(self, query_embedding: np.ndarray, top_k: int = 3) -> Tuple[List[str], List[float]]: """相似度搜索,返回相关文本及其相似度分数""" ids, distances = self.index.knn_query(query_embedding, k=top_k) # 转换为余弦相似度(hnswlib余弦空间返回的是1 - 余弦相似度) similarities = [1 - d for d in distances[0]] # 获取对应文本 texts = [self.labels[i] for i in ids[0]] return texts, similarities class NaiveRAG: """简易RAG系统:文本分块→嵌入→检索→生成""" def __init__(self, embedding_dim: int = 1024, work_dir: str = "work_dir", model: str = "bge-large", chunk_size: int = 500, chunk_overlap: int = 100): self.work_dir = work_dir os.makedirs(work_dir, exist_ok=True) # 初始化组件 self.splitter = LocalSentenceSplitter(chunk_size, chunk_overlap) self.embedding_model = OllamaEmbedding( host="http://localhost:11434", model=model, work_dir=work_dir ) # 获取实际嵌入维度 actual_dim = self.embedding_model.dimension self.vector_store = HNSWVectorStore( embedding_dim=actual_dim, work_dir=work_dir ) logger.info(f"RAG system initialized with chunk_size={chunk_size}, chunk_overlap={chunk_overlap}") def build_index(self, documents: List[str]): """构建索引:分割文档→生成嵌入→存储向量""" all_chunks = [] for doc in documents: chunks = self.splitter.split_text(doc) all_chunks.extend(chunks) logger.info(f"Split {len(documents)} documents into {len(all_chunks)} chunks") # 生成嵌入向量 embeddings = self.embedding_model.encode(all_chunks) # 添加到向量存储 self.vector_store.add_texts(all_chunks, embeddings) self.vector_store.save() def generate_prompt(self, query: str, top_k: int = 3) -> str: """生成提示:检索相关文本→构建提示模板""" # 生成查询嵌入 query_emb = self.embedding_model.encode([query])[0] # 检索相关文本 retrieved_texts, similarities = self.vector_store.similarity_search(query_emb, top_k) # 构建上下文 context = "\n\n".join( [f"[相似度: {sim:.2f}] {text}" for text, sim in zip(retrieved_texts, similarities)] ) # 构建提示 prompt = naive_prompt + ( f"### 问题:\n{query}\n\n" f"### 相关文本:\n{context}\n\n" f"### 回答:" ) return prompt ``` ### 代码解释: #### 1. 文本分块 (LocalSentenceSplitter) - **改进的分句算法**:使用正则表达式分割句子并保留标点符号 - **智能分块逻辑**: - 处理长度超过chunk_size的超长句子 - 确保每个块不超过指定大小 - 实现块间重叠机制 - 动态调整块大小和重叠区域 - **日志记录**:记录分块过程的关键信息 #### 2. 嵌入模型 (OllamaEmbedding) - **连接验证**:初始化时测试Ollama连接并获取嵌入维度 - **错误处理**:完善的错误处理和日志记录 - **超时控制**:可配置的超时设置 - **维度自适应**:动态获取嵌入维度 #### 3. 向量存储 (HNSWVectorStore) - **索引管理**: - 支持加载现有索引或创建新索引 - 自动调整索引大小 - 保存索引元数据 - **相似度搜索**: - 返回文本及其相似度分数 - 支持top-k查询 - **持久化**:保存索引和标签到磁盘 #### 4. RAG系统 (NaiveRAG) - **完整工作流**: 1. 文档分块 2. 生成嵌入 3. 构建向量索引 4. 查询处理 - **提示工程**: - 包含相似度分数 - 明确回答格式要求 - 结构化上下文信息 - **参数可配置**:支持调整分块大小和重叠区域 ### 探究分块机制的影响: 要探究分块机制对问答效果的影响,可以: 1. 尝试不同的chunk_size值(200, 500, 1000) 2. 调整chunk_overlap比例(0%, 20%, 50%) 3. 测试不同分块策略(句子边界/固定大小) 4. 评估指标:答案准确性、相关性、检索质量 ### 使用示例: ```python # 初始化RAG系统 rag = NaiveRAG( work_dir="my_rag", chunk_size=300, chunk_overlap=50 ) # 构建索引 documents = ["长文本1...", "长文本2...", ...] rag.build_index(documents) # 生成提示 query = "你的问题是什么?" prompt = rag.generate_prompt(query) print(prompt) # 将prompt输入LLM获取回答 ``` ### 优化方向: 1. **添加重排序器(reranker)**:对检索结果进行二次排序 2. **元数据存储**:存储文本来源信息 3. **混合检索**:结合关键词和向量搜索 4. **查询扩展**:使用查询重写技术 5. **性能监控**:添加检索质量评估指标
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值