LLM实现本地知识库问答项目中的一些笔记


image-20230620085336168

AutoModel.from_pretrained()
AuroModel.from_pretrained()是Hugging Face Transformers库中用于加载预训练模型的方法。它可以从本地文件、Hugging Face Hub、URL等多种来源加载预训练模型,并返回一个模型对象,用于后续的预测和训练。

具体来说,AutoModel.from_pretrained()方法的参数如下:

- `checkpoint`:必需参数,表示要加载的模型的名称、路径或URL。
- `config`:可选参数,表示要使用的模型配置。如果未指定,会根据模型名称自动选择一个默认的配置文件。
- `torch_dtype`:可选参数,表示要使用的PyTorch张量数据类型。默认为`torch.float32`,也可以设置为`torch.float16`或`torch.bfloat16`。
- `trust_remote_code`:可选参数,表示是否信任从远程源加载的代码(如Hugging Face Hub)。默认为`False`,也可以设置为`True`。
- `cache_dir`:可选参数,表示要使用的缓存目录。如果未指定,会使用默认的缓存目录(`~/.cache/huggingface/transformers`)。
- `force_download`:可选参数,表示是否强制重新下载模型。默认为`False`,也可以设置为`True`。
- `resume_download`:可选参数,表示是否从上次下载的位置恢复下载。默认为`False`,也可以设置为`True`。
- `proxies`:可选参数,表示要使用的代理设置。默认为`None`,也可以设置为代理URL字符串或代理字典。
- `local_files_only`:可选参数,表示是否只从本地文件加载模型。默认为`False`,也可以设置为`True`。

在这段代码中,`LoaderClass.from_pretrained()`方法的第一个参数是`checkpoint`,表示要加载的模型的名称、路径或URL。第二个参数是`config`,表示要使用的模型配置,这里使用了已经加载好的`self.model_config`。第三个参数`torch_dtype`表示要使用的PyTorch张量数据类型,这里根据`self.bf16`的值选择了`torch.bfloat16`或`torch.float16`。第四个参数`trust_remote_code`表示是否信任从远程源(如Hugging Face Hub)加载的代码,这里设置为`True`。
torch.dfloat16和torch.float16
`torch.bfloat16`和`torch.float16`是PyTorch张量的两种不同数据类型,它们在存储精度和内存占用方面有所不同。

具体来说,`torch.bfloat16`是一种16位浮点数数据类型,它可以存储更大的数值范围,但精度相对较低。这种数据类型通常用于深度神经网络的训练和推理中,可以加速运算并减少内存占用。但是,由于精度较低,可能会对模型的准确性产生一定的影响。在使用`torch.bfloat16`时,需要注意数值范围的限制,以避免数值溢出或下溢的问题。

而`torch.float16`是另一种16位浮点数数据类型,它可以存储较小的数值范围,但精度相对较高。这种数据类型通常用于计算机视觉、自然语言处理等任务中,可以提高模型的准确性。但是,由于存储精度较高,内存占用和计算复杂度也相应较高。

因此,选择使用哪种数据类型取决于具体的任务需求和硬件限制。在使用`torch.bfloat16`时,需要注意数值范围的限制,以避免数值溢出或下溢的问题;在使用`torch.float16`时,则需要注意内存占用和计算复杂度的问题。
from accelerate import dispatch_model的解释
`dispatch_model`是`accelerate`库中的一个函数,用于根据当前硬件环境和设置自动选择加速器,以加速模型的训练和推理。具体来说,`dispatch_model`函数会根据以下几个因素自动选择加速器:

1. 硬件环境:加速器的选择会根据计算机的GPU类型、数量和性能等因素进行优化。

2. PyTorch版本:加速器的选择会根据使用的PyTorch版本进行优化,以最大程度地提高性能。

3. 加速器类型:根据用户设置的加速器类型(例如混合精度训练、分布式训练等),选择最佳的加速器实现。

使用`dispatch_model`函数可以简化模型训练和推理的代码编写,并提高模型的训练和推理效率。具体来说,你可以将模型对象传递给`dispatch_model`函数,该函数会自动选择最佳的加速器,并返回一个已经使用加速器的模型对象。例如,下面是一个使用`dispatch_model`函数进行模型训练的示例:

```
from accelerate import dispatch_model

# 加载预训练模型
model = LoaderClass.from_pretrained(checkpoint)

# 自动选择加速器
model, optimizer, train_loader, valid_loader = dispatch_model(model, optimizer, train_loader, valid_loader)

# 训练模型
for epoch in range(num_epochs):
    for batch in train_loader:
        ...
```

在这个示例中,`dispatch_model`函数会自动选择最佳的加速器,并将加速器应用到模型、优化器和数据加载器中,从而提高模型的训练效率。
.float()
在PyTorch中,`.float()`是`Tensor`类的一个方法,用于将`Tensor`对象转换为浮点数类型。在模型中使用`.float()`通常用于将输入数据的数据类型转换为浮点数类型,以便于模型能够正确处理数据。这是因为在PyTorch中,模型的输入和参数通常是浮点数类型,如果输入数据的数据类型与模型期望的数据类型不一致,可能会导致模型无法正确处理数据。

下面是一个示例,展示了在模型中使用`.float()`方法将输入数据转换为浮点数类型:

```python
import torch.nn as nn

# 定义一个模型
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3)
        self.fc1 = nn.Linear(16 * 6 * 6, 10)

    def forward(self, x):
        x = self.conv1(x)
        x = x.view(-1, 16 * 6 * 6)
        x = self.fc1(x)
        return x

# 实例化模型并将其移动到GPU上
model = MyModel().cuda()

# 定义一个输入数据
input_data = torch.randn(1, 3, 32, 32)

# 将输入数据转换为浮点数类型并将其移动到GPU上
input_data = input_data.float().cuda()

# 使用模型进行推理
outputs = model(input_data)
```

在这个示例中,我们首先定义了一个`MyModel`模型,并将其移动到GPU上。然后,我们定义了一个输入数据`input_data`,并使用`.float()`方法将其转换为浮点数类型,并将其移动到GPU上。最后,我们使用模型对输入数据进行推理,并得到输出结果。通过将输入数据转换为浮点数类型,我们可以确保模型能够正确处理数据,并避免出现数据类型不匹配的错误。
load_in_8bit的作用
load_in_8bit`是一个布尔类型的变量,通常用于控制数据加载时的精度。具体来说,如果`load_in_8bit`为True,则数据将以8位整数的形式加载,否则将以32位浮点数的形式加载。

在机器学习中,为了提高模型训练和推理的效率,通常会采用低精度的数据表示方式,例如8位整数。这种方式可以减少数据的存储和传输开销,同时也可以加速计算过程。但是,低精度的数据表示方式可能会对模型的性能和精度产生一定的影响,因此需要根据具体的情况进行选择。

在具体的实现中,`load_in_8bit`通常会作为一个参数传递给数据加载器或模型的构造函数,用于控制数据的加载精度。例如,在PyTorch中,可以通过设置`torch.utils.data.DataLoader`类的`dtype`参数来控制数据的加载精度。而在TensorFlow中,可以通过设置数据加载器的`dtype`参数或模型的`dtype`属性来控制数据的加载精度。
模型对应的tokenizer
在自然语言处理任务中,文本数据需要被转换为数字才能被模型处理。这个转换过程被称为"tokenization",它将原始文本分成一系列"token",并将每个token映射到一个唯一的整数ID。模型接收这些整数ID作为输入,并使用它们来计算输出。

在Hugging Face Transformers库中,每个模型都有一个对应的tokenizer,用于将输入文本转换为模型需要的格式。tokenizer将输入文本分成token,并将每个token映射到一个整数ID。tokenizer还可以执行其他的预处理步骤,例如截断、填充等等。

if type(model) is transformers.LlamaForCausalLM:tokenizer = LlamaTokenizer.from_pretrained(checkpoint, clean_up_tokenization_spaces=True)
回答你的问题,这段代码检查模型是否是`LlamaForCausalLM`类的实例。如果是,就使用`LlamaTokenizer`类的`from_pretrained`方法加载相应的tokenizer。这个tokenizer将输入文本分成token,并将每个token映射到一个整数ID,以便模型可以处理。在这个代码中,tokenizer的作用是将输入文本转换为模型需要的格式,以便模型可以计算输出。

需要注意的是,不同的模型可能需要不同的tokenizer。如果使用错误的tokenizer,可能会导致模型无法正确处理输入数据。因此,在使用模型之前,需要确保使用正确的tokenizer对输入数据进行预处理。
cls = get_class_from_dynamic_module(class_reference=”fnlp/moss-moon-003-sft–modeling_moss.MossForCausalLM”, pretrained_model_name_or_path=checkpoint)什么意思
这段代码的作用是从动态模块中获取一个类对象。具体来说,该函数接受两个参数:

- `class_reference`:类的引用。在这个例子中,引用是 `fnlp/moss-moon-003-sft--modeling_moss.MossForCausalLM`,这意味着该函数将尝试从 `fnlp/moss-moon-003-sft--modeling_moss` 动态模块中获取一个名为 `MossForCausalLM` 的类。
- `pretrained_model_name_or_path`:预训练模型的名称或路径。该参数用于实例化 `MossForCausalLM` 类的对象。

该函数的实现可能类似于以下代码:

```python
def get_class_from_dynamic_module(class_reference,pretrained_model_name_or_path):
    # 从类引用中获取模块名称和类名称
    module_name, class_name = class_reference.rsplit(".", 1)
    # 动态导入模块
    module = importlib.import_module(module_name)
    # 从模块中获取类对象
    cls = getattr(module, class_name)
    # 实例化对象并返回
    return cls.from_pretrained(pretrained_model_name_or_path)
```

这个函数首先将类引用分解成模块名称和类名称。然后,它动态地导入模块,并从模块中获取类对象。最后,它使用给定的预训练模型名称或路径实例化类对象,并返回该对象。
分析代码with init_empty_weights():
model = LoaderClass.from_config(self.model_config,trust_remote_code = True)
model.tie_weights()
这段代码的作用是使用一个空的权重初始化模型,然后将模型的权重绑定到模型中的所有共享层。这个操作通常用于加载已经训练好的模型的配置,但是不想使用它的权重。在这种情况下,我们可以使用一个空的权重初始化模型,然后将模型的权重绑定到模型中的共享层。这样做的好处是,可以在不使用已有模型的权重的情况下,使用已有模型的配置来构建一个新的模型。在这段代码中,`LoaderClass`是一个用于加载模型的类,`from_config`方法用于根据给定的配置创建一个新的模型对象,`trust_remote_code`参数设置为`True`表示信任远程代码,可以用于从远程加载模型。`tie_weights`方法用于将模型中的共享层绑定到一起,以便它们可以共享相同的权重,这样可以减少模型参数的数量,提高模型的训练效率。
`with`语句是Python中的一个上下文管理器,用于管理资源的申请和释放。在这段代码中,`with init_empty_weights():`语句使用了一个自定义的上下文管理器`init_empty_weights()`,这个上下文管理器在进入上下文时执行一些操作,然后在退出上下文时再执行一些操作。在这里,`init_empty_weights()`上下文管理器的作用是用一个空的权重初始化模型,当退出上下文时,模型将被保存在`model`变量中。

使用`with`语句的好处是,在退出上下文时,Python会自动调用上下文管理器的`__exit__()`方法,释放资源并执行一些清理操作。这样可以避免在代码中显式地调用资源释放和清理操作,从而使代码更加简洁和可读。同时,使用`with`语句还可以确保资源的正确释放,无论代码中是否出现了异常或错误,都可以保证资源被正确释放。
在python中,模块和包的区别是什么
在Python中,模块和包都是组织代码的方式,但它们的作用和使用方式略有不同。

模块是Python中的一个概念,它是一个包含Python代码的文件,通常以`.py`为扩展名。模块可以包含定义变量、函数、类和语句的代码,可以被其他模块或程序导入和使用。在Python中,可以使用`import`语句导入一个模块,并使用模块中定义的变量、函数和类等。

包是Python中另一个概念,它是一个包含多个模块的目录。包通常包含一个特殊的`__init__.py`文件,用于指定包的初始化代码和导入的模块列表。在Python中,可以使用`import`语句导入一个包,也可以使用`from ... import`语句导入一个包中的模块、变量、函数和类等。

因此,模块和包的区别在于:

1. 模块是一个文件,包含Python代码;包是一个目录,包含多个模块和一个`__init__.py`文件。

2. 模块通常包含一些特定的功能,可以被其他模块或程序导入和使用;包通常包含多个相关的模块,用于组织和管理代码。

3. 在Python中,可以使用`import`语句导入一个模块或包,但是导入包时需要指定具体的模块或子包名称。

需要注意的是,模块和包是Python中非常重要的概念,可以帮助我们更好地组织和管理代码。在实际编程中,应该根据需要合理地使用模块和包,以提高代码的可读性、可维护性和可重用性。
logging文件日志
在 Python 中,可以使用 `logging` 模块来输出日志。该模块提供了一种灵活且可定制的方式来记录日志信息,并可以控制日志的输出级别、格式和目标等。下面是一个简单的示例,演示如何使用 `logging` 模块输出日志:

```python
import logging

# 配置日志信息
logging.basicConfig(filename='example.log', level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')

# 输出日志信息
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
```

在上面的示例中,我们首先使用 `basicConfig()` 方法配置了日志信息,指定了日志文件的名称为 `example.log`,日志级别为 `DEBUG`,日志格式为 `%(asctime)s %(levelname)s %(message)s`。然后,我们使用 `logging` 模块的不同方法输出不同级别的日志信息。

输出的日志信息将被写入到指定的日志文件中,并且根据配置的日志级别,只有等于或高于该级别的日志信息才会被输出。例如,在上面的示例中,由于指定的日志级别为 `DEBUG`,因此所有级别的日志信息都会被输出到日志文件中。

当然,您还可以根据需要进一步配置日志信息,如指定日志文件的最大大小、备份数量等,以及将日志信息输出到标准输出、邮件等目标。更多关于 `logging` 模块的详细信息可以参考官方文档:https://docs.python.org/3/library/logging.html。
向量库生成中FAISS类探究
    #FAISS向量的初始化
    def __init__(
        self,
        embedding_function: Callable,  #一个可以调用的对象(函数或者方法),用于将文本转化为向量
        index: Any,       #向量索引库,用于存储向量和实现检索功能
        docstore: Docstore,   #文本存储库,用于存储原始文本和文本的元数据信息
        index_to_docstore_id: Dict[int, str],   #一个字典,用于将向量索引库的ID映射到文本存储库中的ID
        relevance_score_fn: Optional[ 
            Callable[[float], float]
        ] = _default_relevance_score_fn,    #一个可调用对象(函数或者方法),用于计算检索结果的相关性得分(可选参数,默认为内置的相关性得分函数)
        normalize_L2: bool = False,      #是否对向量进行L2归一化
    ):
        """Initialize with necessary components."""
        self.embedding_function = embedding_function
        self.index = index
        self.docstore = docstore
        self.index_to_docstore_id = index_to_docstore_id
        self.relevance_score_fn = relevance_score_fn
        self._normalize_L2 = normalize_L2

        
###index如何指定和定义
IndexFlatL2:基于 L2 距离度量的 Flat 索引,适用于小规模的向量检索和精确检索。
IndexIVFFlat:基于倒排索引(Inverted File)的 Flat 索引,适用于大规模的向量检索和近似检索。
IndexIVFPQ:基于倒排索引和 Product Quantization 的索引,适用于大规模的向量检索和高效的近似检索。


eg:	
import faiss
d = 128  # 向量维度
index = faiss.IndexFlatL2(d)
    在上面的代码中,我们首先定义了向量的维度 d,然后使用 faiss.IndexFlatL2 类创建了一个基于 L2 距离度量的 Flat 索引,并将其赋值给变量 index。这个索引对象可以用来存储向量和进行精确的向量检索。
###



###方法
def add_texts(
        self,
        texts: Iterable[str],
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> List[str]:
    """
    向索引中添加文本数据。

    :param texts: 一个包含多个文本的可迭代对象,每个元素表示一个文本。
    :param metadatas: 一个可选的列表,表示每个文本的元数据。每个元素是一个字典,包含文本的其它属性信息。
    :param ids: 一个可选的列表,表示每个文本的 ID。如果不提供,则默认为 0, 1, 2, ...
    :param kwargs: 其它可选参数,用于控制文本处理和特征提取的过程。
    :return: 一个包含每个文本的 ID 的列表。
    """
    生成向量的方式:embeddings = [self.embedding_function(text) for text in texts]   #预训练的模型,用的Transformer
    vector = np.array(embeddings, dtype=np.float32)
    
    
    
    
###
def add_embeddings(
        self,
        text_embeddings: Iterable[Tuple[str, List[float]]],
        metadatas: Optional[List[dict]] = None,
        ids: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> List[str]:
    """
    向索引中添加文本特征向量数据。

    :param text_embeddings: 一个包含多个文本特征向量的可迭代对象,每个元素是一个元组 (text, embedding),表示一个文本和其对应的特征向量。
    :param metadatas: 一个可选的列表,表示每个文本的元数据。每个元素是一个字典,包含文本的其它属性信息。
    :param ids: 一个可选的列表,表示每个文本的 ID。如果不提供,则默认为 0, 1, 2, ...
    :param kwargs: 其它可选参数,用于控制特征处理和特征提取的过程。
    :return: 一个包含每个文本的 ID 的列表。
    """
    
    
    
    
    
###
def similarity_search_with_score_by_vector(
        self,
        embedding: List[float],
        k: int = 4,
        filter: Optional[Dict[str, Any]] = None,
        fetch_k: int = 20,
        **kwargs: Any,
    ) -> List[Tuple[Document, float]]:
    """
    根据输入的特征向量进行相似度检索,并返回文本列表及其与输入向量的相似度得分。

    :param embedding: 一个包含文本特征向量的列表,表示输入的特征向量。
    :param k: 一个整数,表示需要检索的最近邻近似度数量。
    :param filter: 一个可选的字典,表示需要过滤的条件。字典的键表示过滤条件,值表示过滤条件对应的值。
    :param fetch_k: 一个整数,表示需要从索引中获取的文本数量。
    :param kwargs: 其它可选参数,用于控制特征处理和特征提取的过程。
    :return: 一个包含元组的列表,每个元组表示一个检索到的文本及其与输入向量的相似度得分。
    """
***注意***
`k` 和 `fetch_k` 参数并不直接冲突,因为它们控制的是不同的内容。

`k` 参数控制的是需要检索的最近邻近似度数量,而 `fetch_k` 参数控制的是需要从索引中获取的文本数量。在进行相似度检索时,首先会从索引中获取 `fetch_k` 个文本,然后从这些文本中选择与输入向量相似度最高的 `k` 个文本。

例如,如果 `k=4`,`fetch_k=20`,则表示需要从索引中获取与输入向量相似度最高的 20 个文本,然后再从这 20 个文本中选择与输入向量相似度最高的 4 个文本。

实际上,`fetch_k` 的取值可以比 `k` 大,这样可以保证在选择与输入向量相似度最高的 `k` 个文本时,有更多的文本可供选择,从而提高检索的准确性。但是,设置过大的 `fetch_k` 值可能会影响检索效率和内存消耗,需要根据具体情况进行调整。


这个方法是对上面similarity_search_with_score_by_vector的封装。
def similarity_search_with_score(
        self,
        query: str,
        k: int = 4,
        filter: Optional[Dict[str, Any]] = None,
        fetch_k: int = 20,
        **kwargs: Any,
    ) -> List[Tuple[Document, float]]:
    """
    根据输入的文本进行相似度检索,并返回文本列表及其与输入文本的相似度得分。

    :param query: 一个字符串,表示输入的文本。
    :param k: 一个整数,表示需要检索的最近邻近似度数量。
    :param filter: 一个可选的字典,表示需要过滤的条件。字典的键表示过滤条件,值表示过滤条件对应的值。
    :param fetch_k: 一个整数,表示需要从索引中获取的文本数量。
    :param kwargs: 其它可选参数,用于控制特征处理和特征提取的过程。
    :return: 一个包含元组的列表,每个元组表示一个检索到的文本及其与输入文本的相似度得分。
    """
filter 参数是一个可选的字典,表示需要过滤的条件。字典的键表示过滤条件,值表示过滤条件对应的值。例如,可以使用 filter={'category': 'sports'} 来过滤类别为 'sports' 的文本。

#查询得分
for doc, score in results:
    print(doc.text, score)

    
    
    
    
###
def similarity_search_by_vector(
        self,
        embedding: List[float],
        k: int = 4,
        filter: Optional[Dict[str, Any]] = None,
        fetch_k: int = 20,
        **kwargs: Any,
    ) -> List[Document]:
    embedding:一个浮点数列表,表示查询向量的嵌入(embedding)。
	k:一个整数,表示要返回的最相似文档的数量。默认为 4filter:一个字典,表示要过滤的文档属性。该字典的键值对表示要过滤的属性和其对应的值。默认	   为 None,表示不进行过滤。
	fetch_k:一个整数,表示要从倒排索引中检索的文档数量。默认为 20**kwargs:一个可变关键字参数,表示其他参数
    
    
这个方法的实现依据similarity_search_with_score方法,不过只取出了前面部分
def similarity_search(
        self,
        query: str,
        k: int = 4,
        filter: Optional[Dict[str, Any]] = None,
        fetch_k: int = 20,
        **kwargs: Any,
    ) -> List[Document]:
    query:一个字符串,表示查询的文本。
	k:一个整数,表示要返回的最相似文档的数量。默认为 4filter:一个字典,表示要过滤的文档属性。该字典的键值对表示要过滤的属性和其对应的值。默认	为 None,表示不进行过滤。
	fetch_k:一个整数,表示要从倒排索引中检索的文档数量。默认为 20**kwargs:一个可变关键字参数,表示其他参数。
    
    
###
def max_marginal_relevance_search_by_vector(
        self,
        embedding: List[float],
        k: int = 4,
        fetch_k: int = 20,
        lambda_mult: float = 0.5,
        filter: Optional[Dict[str, Any]] = None,
        **kwargs: Any,
    ) -> List[Document]:
    embedding:一个浮点数列表,表示查询向量的嵌入(embedding)。
	k:一个整数,表示要返回的最相似文档的数量。默认为 4。
	fetch_k:一个整数,表示要从倒排索引中检索的文档数量。默认为 20。
	lambda_mult:一个浮点数,表示查询向量和文档向量之间的权重。默认为 0.5filter:一个字典,表示要过滤的文档属性。该字典的键值对表示要过滤的属性和其对应的值。默认为 None,表示不进行过滤。
	**kwargs:一个可变关键字参数,表示其他参数。
    
    
    
 ***该函数的算法
    
    当我们需要在一个文档集合中进行相似度搜索时,我们可以使用倒排索引等技术来快速定位与查询文本或向量最相似的文档。然而,仅仅根据相似度来排序文档列表并不一定能得到最好的结果。例如,在一个新闻搜索引擎中,如果用户查询 "巴西足球",我们可能会得到很多相关的新闻文章,但这些文章之间可能会有很多重复的内容和观点,这样就不能满足用户的需求。

为了解决这个问题,我们可以使用最大边际相关性 (MMR) 算法来对文档列表进行排序和筛选。MMR 算法通过将查询向量和已经选择的文档向量之间的相似度进行权衡,选择与查询向量最相关且彼此之间不太相似的文档。这样,我们可以得到一组有代表性、多样性和相关性的文档列表,从而更好地满足用户的需求。

具体来说,在实现 `max_marginal_relevance_search_by_vector` 函数时,我们可以按照以下步骤进行:

1. 使用倒排索引查找与查询向量最相似的文档,并将它们按相似度从高到低排列。
2. 初始化一个文档列表 `selected_docs`,其中包含与查询向量最相似的前 `k` 个文档。
3. 对于剩余的文档,计算它们与查询向量的相似度和它们与已选择文档的相似度,并计算它们的 MMR 值。MMR 值可以通过以下公式计算:

   ````

   MMR(d) = λ × sim(d, q) - (1 - λ) × max(sim(d, d') for d' in selected_docs)
其中,`d` 表示当前文档,`q` 表示查询向量,`λ` 表示查询向量和文档向量之间的权重,`sim(d, q)` 表示文档 `d` 和查询向量 `q` 之间的相似度,`sim(d, d')` 表示文档 `d` 和已选择文档 `d'` 之间的相似度。

公式的意义是,选择与查询向量最相关的文档,并减少与已选择文档之间的相似度,从而得到与查询向量最相关且彼此之间不太相似的文档。
4. 将剩余文档按照 MMR 值从高到低排序,并选择前 k 个文档加入 selected_docs 中。
5. 返回 selected_docs

需要注意的是,MMR 算法的性能和效果受到参数 λ 的影响。当 λ 的值较小时,算法更加注重文档之间的多样性,而当 λ 的值较大时,算法更加注重文档与查询的相似度。在实践中,可以根据具体的应用场景和用户需求来调整 λ 的值。

def max_marginal_relevance_search(
self,
query: str,
k: int = 4,
fetch_k: int = 20,
lambda_mult: float = 0.5,
filter: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> List[Document]:

参数:

  • query:一个字符串,表示查询文本。
  • k:一个整数,表示要返回的最相似文档的数量。默认为 4。
  • fetch_k:一个整数,表示要从倒排索引中检索的文档数量。默认为 20。
  • lambda_mult:一个浮点数,表示查询向量和文档向量之间的权重。默认为 0.5。
  • filter:一个字典,表示要过滤的文档属性。该字典的键值对表示要过滤的属性和其对应的值。默认为 None,表示不进行过滤。
  • **kwargs:一个可变关键字参数,表示其他参数。

返回值:

  • 一个 List[Document] 类型的列表,表示与查询文本最相关的文档列表。该列表中的每个元素都是一个 Document 对象,包含与查询文本最相关的文档的信息,例如文本、得分、ID 等。

具体来说,该函数的作用与 max_marginal_relevance_search_by_vector 函数类似,不同之处在于它接受一个查询文本而不是查询向量作为输入,并将查询文本转换为向量后调用 max_marginal_relevance_search_by_vector 函数来实现相似度搜索和检索。在函数内部,可以使用语言模型或词嵌入模型将查询文本转换为向量,然后将向量传递给 max_marginal_relevance_search_by_vector 函数进行后续处理。该函数通常用于在文档集合中进行相似度搜索和检索。

def merge_from(self, target: FAISS) -> None:

这是一个方法名为 merge_from 的函数,它定义在一个名为 FAISS 的对象中。这个函数的作用是将另一个 FAISS 对象合并进当前对象。

具体来说,当我们需要将两个 FAISS 对象合并成一个时,可以使用 merge_from 方法。该方法的参数 target 是一个 FAISS 对象,表示要合并进当前对象的目标对象。合并后,目标对象的文档将被添加到当前对象的文档存储器 (docstore) 中,并且目标对象的索引 (index) 将被合并到当前对象的索引中。
###

                                                                           def __from(
  cls,
  texts: List[str],
  embeddings: List[List[float]],
  embedding: Embeddings,
  metadatas: Optional[List[dict]] = None,
  ids: Optional[List[str]] = None,
  normalize_L2: bool = False,
  **kwargs: Any,

) -> FAISS:
这是一个类方法,用于从文本和嵌入列表中构建一个 FAISS 对象。该函数的参数如下:

  • cls:类本身。
  • texts:一个字符串列表,包含待索引的文本。
  • embeddings:一个浮点数列表的列表,表示每个文本对应的嵌入向量。
  • embedding:一个 Embeddings 对象,表示用于生成嵌入向量的嵌入器。
  • metadatas:一个字典列表,包含每个文档的元数据。默认为 None
  • ids:一个字符串列表,表示每个文档的唯一标识符。默认为 None
  • normalize_L2:一个布尔值,表示是否对嵌入向量进行L2范数归一化。默认为 False
  • **kwargs:一个可变关键字参数,表示其他参数。

返回值:一个 FAISS 对象,表示构建的索引。

该方法的作用是将文本列表和嵌入列表转换为 FAISS 对象,以进行相似度搜索和检索。在函数内部,可以使用 embedding 对象将文本转换为嵌入向量,然后将嵌入向量传递给 FAISS 对象进行索引和搜索。

具体而言,该函数首先创建一个 FAISS 对象,并将嵌入向量添加到索引中。如果指定了元数据和文档 ID,则将它们与嵌入向量一起添加到索引中。为了快速搜索,该方法会对嵌入向量进行 L2 范数归一化,这可以提高搜索效率和准确性。

最后,该方法返回创建的 FAISS 对象。你可以使用该对象执行相似度搜索和检索,并获取与查询文本最相关的文档列表。
###
def from_texts(
cls,
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> FAISS:
使用上一个方法的__from进行实现
这是一个类方法,用于从文本列表中构建一个 FAISS 对象。该函数的参数如下:

  • cls:类本身。
  • texts:一个字符串列表,包含待索引的文本。
  • embedding:一个 Embeddings 对象,表示用于生成嵌入向量的嵌入器。
  • metadatas:一个字典列表,包含每个文档的元数据。默认为 None
  • ids:一个字符串列表,表示每个文档的唯一标识符。默认为 None
  • **kwargs:一个可变关键字参数,表示其他参数。

返回值:一个 FAISS 对象,表示构建的索引。

该方法的作用是将文本列表转换为 FAISS 对象,以进行相似度搜索和检索。在函数内部,可以使用 embedding 对象将文本转换为嵌入向量,然后将嵌入向量传递给 FAISS 对象进行索引和搜索。

具体而言,该函数首先使用 embedding 对象将文本列表转换为嵌入向量列表。然后,创建一个 FAISS 对象,并将嵌入向量添加到索引中。如果指定了元数据和文档 ID,则将它们与嵌入向量一起添加到索引中。为了快速搜索,该方法会对嵌入向量进行 L2 范数归一化,这可以提高搜索效率和准确性。

最后,该方法返回创建的 FAISS 对象。你可以使用该对象执行相似度搜索和检索,并获取与查询文本最相关的文档列表。

                                                                           ###

def from_embeddings(
cls,
text_embeddings: List[Tuple[str, List[float]]],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
ids: Optional[List[str]] = None,
**kwargs: Any,
) -> FAISS: 这是一个类方法,用于从文本嵌入元组列表中构建一个 FAISS 对象。该函数的参数如下:

  • cls:类本身。
  • text_embeddings:一个元组列表,包含每个文本的唯一标识符和对应的嵌入向量。
  • embedding:一个 Embeddings 对象,表示用于生成嵌入向量的嵌入器。
  • metadatas:一个字典列表,包含每个文档的元数据。默认为 None
  • ids:一个字符串列表,表示每个文档的唯一标识符。默认为 None
  • **kwargs:一个可变关键字参数,表示其他参数。

返回值:一个 FAISS 对象,表示构建的索引。

该方法的作用是将文本嵌入元组列表转换为 FAISS 对象,以进行相似度搜索和检索。在函数内部,可以使用 embedding 对象将嵌入向量添加到 FAISS 对象的索引中。

具体而言,该函数首先创建一个 FAISS 对象,并将嵌入向量添加到索引中。如果指定了元数据和文档 ID,则将它们与嵌入向量一起添加到索引中。为了快速搜索,该方法会对嵌入向量进行 L2 范数归一化,这可以提高搜索效率和准确性。

最后,该方法返回创建的 FAISS 对象。你可以使用该对象执行相似度搜索和检索,并获取与查询文本最相关的文档列表。

                                                                           def save_local(self, folder_path: str, index_name: str = "index") -> None:

这是一个实例方法,用于将 FAISS 对象保存到本地文件夹。该函数的参数如下:

  • folder_path:一个字符串,表示本地文件夹的路径,用于保存索引文件。
  • index_name:一个字符串,表示索引文件的名称。默认为 "index"

该方法的作用是将 FAISS 对象保存到本地文件夹中,以便在以后的时间内进行加载和使用。在函数内部,该方法使用 faiss.write_index 函数将索引对象写入文件,并保存到指定的文件夹中。

具体而言,该方法首先检查文件夹是否存在,如果不存在,则创建一个新的文件夹。然后,使用 faiss.write_index 函数将索引对象写入文件,并将文件保存到指定的文件夹中。保存的索引文件可以在以后的时间内使用 load_local 方法加载。

最后,该方法返回 None
###
def load_local(
cls, folder_path: str, embeddings: Embeddings, index_name: str = “index”
) -> FAISS:
这是一个类方法,用于从本地文件夹中加载 FAISS 索引对象。该函数的参数如下:

  • cls:类本身。
  • folder_path:一个字符串,表示本地文件夹的路径,用于加载索引文件。
  • embeddings:一个 Embeddings 对象,表示用于生成嵌入向量的嵌入器。
  • index_name:一个字符串,表示索引文件的名称。默认为 "index"

返回值:一个 FAISS 对象,表示已加载的索引。

该方法的作用是从本地文件夹中加载 FAISS 索引对象,并返回该对象以供使用。在函数内部,该方法使用 faiss.read_index 函数从文件中读取索引对象,并使用 embeddings 对象生成嵌入向量。然后,将嵌入向量添加到索引中,并返回创建的 FAISS 索引对象。

具体而言,该方法首先使用 faiss.read_index 函数从指定的文件夹中读取索引对象。然后,使用 embeddings 对象将存储在索引中的嵌入向量重新生成,并将它们添加到索引对象中。最后,该方法返回创建的 FAISS 对象,以供进行文本检索和相似度搜索。

需要注意的是,加载的索引对象必须与嵌入器对象 embeddings 兼容,即索引对象和嵌入器对象必须使用相同的嵌入策略和参数。否则,在进行文本检索和相似度搜索时可能会导致不准确的结果。

def _similarity_search_with_relevance_scores(

    self,
    query: str,
    k: int = 4,
    filter: Optional[Dict[str, Any]] = None,
    fetch_k: int = 20,
    **kwargs: Any,
) -> List[Tuple[Document, float]]:
                                                                           

这是一个私有方法,用于执行带有相关度得分的相似度搜索。该函数的参数如下:

  • query:一个字符串,表示查询文本。
  • k:一个整数,表示每个查询的返回文档数。默认为 4
  • filter:一个字典,表示用于过滤搜索结果的过滤器。默认为 None
  • fetch_k:一个整数,表示在过滤之后要返回的文档数。默认为 20
  • **kwargs:一个可变关键字参数,表示其他参数。

返回值:一个元组列表,表示查询文本与每个返回文档的相关度得分。

该方法的作用是执行带有相关度得分的相似度搜索,并返回与查询文本最相关的文档列表。在函数内部,该方法使用 faiss.StandardGpuResources 创建 GPU 资源,并使用 embeddings 对象将查询文本转换为嵌入向量。然后,使用 faiss.Index.search 函数执行相似度搜索,并根据相似度得分对搜索结果进行排序。最后,该方法返回查询文本与每个返回文档的相关度得分,并将它们封装为元组列表。

具体而言,该方法首先使用 faiss.StandardGpuResources 创建 GPU 资源,并使用 embeddings 对象将查询文本转换为嵌入向量。然后,使用 faiss.Index.search 函数执行相似度搜索,并获取与查询文本最相关的文档列表。在获取文档列表后,该方法对返回文档进行排序,并计算每个文档的相关度得分。如果指定了过滤器,则使用过滤器从搜索结果中过滤文档。最后,该方法将查询文本与每个返回文档的相关度得分封装为元组,并返回元组列表。

需要注意的是,该方法使用 GPU 资源执行相似度搜索,因此需要确保计算机上安装了支持 GPU 计算的 faiss 库,并且 GPU 资源可用。


##### index定义是Any对象,我不知道它下一步调用的是哪个

````
`cores, indices = self.index.search(vector, k)` 是 `faiss` 库中 `Index` 类的一个方法,用于在索引中查找与给定向量最相似的 k 个向量。它返回两个数组:`cores` 和 `indices`。其中,`cores` 表示每个查询向量的相似度得分(即余弦相似度),`indices` 表示与每个查询向量最相似的 k 个向量的索引号。

请确保您已经正确导入 `faiss` 库,并在代码中创建了 `Index` 对象并将其存储在 `self.index` 变量中。如果您的代码中仍然找不到 `search` 方法,请检查您的代码是否正确导入 `faiss` 库,并创建了正确的 `Index` 对象。
`Any` 是 Python 中的一个类型注解,它表示可以是任何类型的对象。因此,当您定义一个类型为 `Any` 的变量时,它可以引用任何类型的对象,包括自定义类型、内置类型、模块、函数等等。

在您的代码中,`index` 变量的类型为 `Any`,这意味着它可以引用任何类型的对象。如果您不确定它引用的是哪种类型的对象,可以尝试使用 `type` 函数来查看它的类型,例如:

print(type(index))


这将输出 `index` 变量的类型。一旦您知道了 `index` 变量所引用的对象的类型,就可以查找该对象的文档或源代码,以了解它支持哪些方法和属性。如果该对象是 `faiss` 库中的 `Index` 类的实例,那么它应该支持 `search` 方法,用于在索引中查找与给定向量最相似的向量。
````

##### scores, indices = self.index.search(vector, k)#scores 表示每个查询向量的相似度得分(即余弦相似度),indices 表示与每个查询向量最相似的 k 个向量的索引号。for j,i in enumerate(indices[0]):这个地方为什么用indices[0],他有什么含义

````
在 `faiss` 中,`search` 方法可以同时查询多个向量,并返回每个查询向量与数据库中其他向量的相似度得分和索引号。如果查询向量的数量为 `n`,则 `search` 方法返回的 `scores` 和 `indices` 是两个具有 `shape = (n, k)` 的数组,其中 `k` 表示每个查询向量返回的最相似向量的数量。

例如,在以下代码中:

```python
scores, indices = self.index.search(queries, k)

如果 queries 是一个 md 列的二维数组,表示 m 个查询向量,每个向量有 d 个维度,则 scoresindicesshape 分别为 (m, k)(m, k)

在该算法中,每次只查询一个向量,因此 scoresindices 都是一个一维数组,其中 scores[0] 表示查询向量与 k 个最相似向量的相似度得分,indices[0] 表示 k 个最相似向量在索引中的编号。

因此,在循环中只用到了 indices[0],表示第一个查询向量的 k 个最相似向量在索引中的编号。至于 indices[1] 表示什么,由于本算法每次只查询一个向量,因此 indices[1] 没有实际意义,可以忽略。

暂时的理解


LLM streaming reponse       STREAMING = True       这个加不加有什么区别

```
这个代码片段定义了一个名为 `STREAMING` 的变量,它的值为 `True`。这个变量的作用是控制是否启用流式响应(streaming response)。

如果将 `STREAMING` 设置为 `False`,则表示禁用流式响应,这意味着服务端在处理完整个请求后才会向客户端发送响应。在这种情况下,客户端需要等待服务端处理完整个请求后才能开始处理响应,这可能会导致客户端等待时间过长,尤其是在处理大型请求或响应时。

如果将 `STREAMING` 设置为 `True`,则表示启用流式响应,服务端会在处理请求期间不断向客户端发送响应。这种方式可以有效减少客户端等待的时间,同时也可以逐步返回响应结果,使得客户端可以更早地开始处理响应。但是,启用流式响应也会增加服务端的负担,因为服务端需要在处理请求时不断生成响应数据并发送给客户端。

因此,是否启用流式响应取决于具体的应用场景和需求。如果请求和响应较小,可以禁用流式响应以简化实现;如果请求和响应较大,可以考虑启用流式响应以提高性能和用户体验。
```

##### @lru_cache(CACHED_VS_NUM)什么意思是

```python

@lru_cache(CACHED_VS_NUM)
def load_vector_store(vs_path, embeddings):
    return MyFAISS.load_local(vs_path, embeddings)
`@lru_cache(CACHED_VS_NUM)` 是 Python 中的一个装饰器语法,它可以用来缓存函数的结果,以避免重复计算,提高程序的执行效率。

`@lru_cache` 是 Python 标准库 `functools` 中提供的一个装饰器,它可以用来实现一个“最近最少使用”(Least Recently Used,LRU)的缓存机制。LRU 缓存会在缓存满时删除最近最少使用的缓存项,以保持缓存的最新和最常用状态。

`CACHED_VS_NUM` 是一个参数,它用来指定缓存的最大容量,即缓存可以存储的最多的函数调用结果数量。当缓存已满时,新的函数调用会导致最近最少使用的缓存项被删除,以腾出空间给新的缓存项。

因此,`@lru_cache(CACHED_VS_NUM)` 的意思就是使用 LRU 缓存机制来缓存函数的结果,并指定缓存的最大容量为 `CACHED_VS_NUM`。
```

```python
如何减少模型的响应速度呢!

response = {"query": query,
            "result": resp,
            "source_documents": related_docs_with_score}
#这个是response的响应



print(response["source_documents"])
metadata={'source': 'pdf/计算机网络.pdf', 'score': 0})]




{'query': '2', 'result': '','source_documents': [Document(page_content='AS 100 ', metadata={'source': 'pdf/计算机网络.pdf', 'score': 0})]}
```



##### if search_type == "similarity":      return self.similarity_search(query, **kwargs)   elif search_type == "mmr":        return self.max_marginal_relevance_search(query, **kwargs)这两个搜索哪个更好一点

```
这个问题的答案取决于具体的应用场景和需求。`similarity_search` 函数和 `max_marginal_relevance_search` 函数都是用于搜索最相似的向量,但是它们的实现方式略有不同,因此在不同的场景下可能会有不同的表现。

`similarity_search` 函数使用余弦相似度来计算向量之间的相似度得分,并返回与查询向量最相似的前 k 个向量。这种方法简单直接,适用于大多数的基本搜索需求。如果您只需要找到与查询向量最相似的向量,那么 `similarity_search` 函数可能更适合您的需求。

相比之下,`max_marginal_relevance_search` 函数使用最大边际相关性算法(MMR)来计算向量之间的相似度得分,并尝试最大化返回的向量之间的差异性。这种方法可以帮助您找到与查询向量不仅相似,而且尽可能不同的向量,从而提高搜索结果的多样性。如果您需要返回一组不仅相似而且多样化的向量,那么 `max_marginal_relevance_search` 函数可能更适合您的需求。

需要注意的是,使用 `max_marginal_relevance_search` 函数可能会增加计算的复杂度和时间,因为它需要计算向量之间的相似度得分,并进行多次迭代以优化搜索结果。因此,在使用 `max_marginal_relevance_search` 函数时,需要权衡计算的复杂度和搜索结果的质量。
```

##### 如何查看一个模型有哪些层次

要查看一个模型的层次结构,可以使用PyTorch中的named_parameters()方法。这个方法可以返回一个生成器对象,用于迭代模型的所有参数及其对应的名称。通过遍历这个生成器对象,我们可以逐层查看模型的结构,找到每个层的名称以及相应的参数。

以下是一个查看模型层次结构的示例代码:

import torch
from transformers import AutoModel

# 加载一个预训练的模型
model = AutoModel.from_pretrained('bert-base-cased')

# 遍历模型的所有参数及其对应的名称
for name, param in model.named_parameters():
    # 打印参数的名称和形状
    print(f'{name} - {param.shape}')

在这个示例中,我们首先使用AutoModel.from_pretrained()方法加载了一个预训练的BERT模型。然后,我们使用named_parameters()方法遍历了这个模型的所有参数及其对应的名称,并打印了每个参数的名称和形状。通过观察这些输出,我们可以了解这个模型的层次结构,并查找每个层的名称以及相应的参数。


##### 回调函数

def change_chunk_conent(mode, label_conent, history):
    conent = ""
    if "chunk_conent" in label_conent:
        conent = "搜索结果上下文关联"
    elif "one_content_segmentation" in label_conent:  # 这里没用上,可以先留着
        conent = "内容分段入库"

    if mode:
        return gr.update(visible=True), history + [[None, f"【已开启{conent}】"]]
    else:
        return gr.update(visible=False), history + [[None, f"【已关闭{conent}】"]]
        
        
chunk_conent.change(fn=change_chunk_conent,
                                        inputs=[chunk_conent, gr.Textbox(value="chunk_conent", visible=False), chatbot],
                                        outputs=[chunk_sizes, chatbot])
                                        
                                        
`change_chunk_conent` 函数是一个回调函数,用于响应 `chunk_content` 复选框的状态变化,并更新相关的组件状态。在 Gradio 库中,回调函数通常使用 `change()` 方法进行注册,以便在用户交互时自动调用。

在这里,`chunk_conent.change(fn=change_chunk_conent, inputs=[chunk_conent, gr.Textbox(value="chunk_conent", visible=False), chatbot], outputs=[chunk_sizes, chatbot])` 方法注册了 `change_chunk_conent` 函数,并指定了该函数的输入和输出组件。具体来说,`inputs` 参数包括了 `chunk_conent` 复选框、一个隐藏的文本框(用于记录当前的 `chunk_content` 状态)和一个名为 `chatbot` 的聊天机器人组件,`outputs` 参数包括了一个名为 `chunk_sizes` 的下拉框和同样的 `chatbot` 聊天机器人组件。这些输入和输出组件都是在 `gr.Interface()` 中创建并添加到布局中的。

当用户在 Web 界面中勾选或取消 `chunk_content` 复选框时,Gradio 库会自动调用 `change_chunk_conent` 函数,并传递当前 `chunk_content` 复选框的状态、隐藏的文本框的值和聊天机器人的历史记录等参数。`change_chunk_conent` 函数根据输入参数的值进行相应的处理,然后返回一个 `gr.update()` 对象,用于更新 `chunk_sizes` 和 `chatbot` 组件的显示状态和内容。
##### 代码解释
for file in files:
    filename = os.path.split(file.name)[-1]
    shutil.move(file.name, os.path.join(KB_ROOT_PATH, vs_id, "content", filename))
    filelist.append(os.path.join(KB_ROOT_PATH, vs_id, "content", filename))
    
    
    
这段代码的作用是将一个文件列表中的文件从原来的位置移动到指定的目标目录,并将移动后的文件路径添加到一个文件路径列表中。

具体来说,这段代码使用了Python标准库中的`os.path.split()`函数和`shutil.move()`函数。`os.path.split(file.name)`函数将文件路径`file.name`分割成两个部分:目录路径和文件名。`[-1]`索引表示返回分割后的结果中的文件名部分。

然后,`shutil.move()`函数将文件从原来的位置移动到指定的目标目录。移动后的文件名不变,因此移动后的文件路径可以通过将目标目录路径和文件名拼接而得到。

最后,`filelist.append()`函数将移动后的文件路径添加到一个文件路径列表中。

总的来说,这段代码的作用是将一个文件列表中的文件移动到指定的目标目录,并将移动后的文件路径添加到一个文件路径列表中,以备后续使用。
经过多次实验,结合速度,质量的考虑,文本查找量为1,文件扩展长度为150.
{'query': '11', 'prompt': '已知信息:通过远射打进唯一进球, 尽管慢动作显示是打在瑞典队后卫腿上弹射入网, 但从过程到结果, 均显示了相同内容——“瑞典队的防守并非无懈可击”。 \u3000\u3000商瑞华让科研教练曹晓东等人对这场比赛进行精心剪辑, 尤其是瑞典队失球以及美国队形成有威胁射门的片段, 更是被放大进行动作分解,\n根据上述已知信息,简洁且专业地回答用户地问题。如果问题不清晰或无法从已知信息中得到答案,请回复"根据已知信息无法回答该问题,请提供足够的本地知识库信息",切勿在答案中胡编乱造,切勿回答已知信息以外的问题,问题不明确的时,请回答问题不明确,答案请使用中文。问题是:11', 'result': '根据已知信息,可以得出以下结论:\n\n1. 进球是通过远射打进的,尽管慢动作显示是打在瑞典队后卫腿上弹射入网,但过程显示瑞典队的防守并非无懈可击。\n2. 这场比赛的精心剪辑包括对瑞典队失球以及美国队形成有威胁射门的片段进行动作分解。\n3. 商瑞华让科研教练曹晓东等人对这场比赛进行精心剪辑,以确保比赛结果符合预期,尤其是瑞典队的防守并非无懈可击。\n\n因此,可以得出结论:这场比赛的剪辑体现了商瑞华让科研教练曹晓东', 'source_documents': [Document(page_content='通过远射打进唯一进球, 尽管慢动作显示是打在瑞典队后卫腿上弹射入网, 但从过程到结果, 均显示了相同内容——“瑞典队的防守并非无懈可击”。 \u3000\u3000商瑞华让科研教练曹晓东等人对这场比赛进行精心剪辑, 尤其是瑞典队失球以及美国队形成有威胁射门的片段, 更是被放大进行动作分解,', metadata={'source': '1.txt', 'score': 983}, _lc_kwargs={'page_content': '通过远射打进唯一进球,', 'metadata': {'source': '1.txt', 'score': 983}})]}

文章作者: genius dragon
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 genius dragon !
  目录