Coggle 30 Days of ML(24年3月):基金金融问答

Part1 内容介绍

在自然语言处理领域,大型语言模型(LLM)如GPT-3、BERT等已经取得了显著的进展,它们能够生成连贯、自然的文本,回答问题,并执行其他复杂的语言任务。但想要让大模型回答复杂问题,需要对原始内容进行深入的理解,比如对数据库进行解析、图像、语音等内容进行解析。

RAG技术结合了大型语言模型的强大生成能力和检索系统的精确性。SQL Agent是一个先进的工具,旨在实现与SQL数据库的灵活交互。SQL Agent允许用户基于数据库的架构和内容提出问题。这意味着它不仅可以提供关于数据库结构的信息(例如表名、列类型),还可以提供关于这些表中实际数据的信息。

SQL Agent的一个显著特点是其能够优雅地从错误中恢复。它通过运行生成的查询,捕获任何发生的错误或异常,然后正确重新生成查询来实现这一点。这确保了交互的顺畅进行,并防止由于错误而导致的中断。

本月的学习内容主要围绕检索增强生成(RAG)与SQL Agent展开:

  • SQL Agent原理
  • 知识库构建与管理、检索模块技术
  • ChatGPT/ChatGLM的API使用

Part2 活动安排

  • 免费学习活动,不会收取任何费用。
  • 请各位同学添加下面微信,并回复【竞赛学习】,即可参与。

Part3 积分说明和奖励

为了激励各位同学完成的学习任务,在完成学习后(本次活动,截止4月1),将按照积分顺序进行评选 Top3 的学习者。

昵称 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11
无盐 :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark: :white_check_mark:
宁静致远 :white_check_mark: :white_check_mark: :white_check_mark:
暂定 :white_check_mark: :white_check_mark: :white_check_mark:
徐乜乜 :white_check_mark: :white_check_mark: :white_check_mark:
任宇は神様 :white_check_mark: :white_check_mark: :white_check_mark:
真真夜夜 :white_check_mark: :white_check_mark:
soldier0629 :white_check_mark:
Jason :white_check_mark:
陈某升 :white_check_mark:
yang mz :white_check_mark:
月月古 :white_check_mark:
LZY :white_check_mark:
axiba :white_check_mark:
:white_check_mark:

打卡积分奖励

Top1的学习者将获得以下奖励:

  • 50元现金红包
  • Coggle 竞赛专访机会

Top2-3的学习者将获得以下奖励:

  • 20元红包
  • Coggle 竞赛专访机会

历史活动打卡链接,可以参考如下格式:

欢迎大家将打卡内容,发在本论坛中,让大家一起学习进步,交流讨论。

Part4 基金金融问答

背景介绍

本次比赛要求选手基于大语言模型LLM构建一个问答系统,问答内容涉及基金/股票/债券/招股书等不同数据来源。不限制选手的模型使用,选手可以选择商业化模型或者开源模型,可以结合多个模型,可以采用公开访问的外部数据进行微调模型,也可以采用LangChain、向量数据库等相关技术。

为参赛选手提供比赛涉及的全部结构化数据和文本数据,包括10张基金表数据和80篇招股书文档,比赛所有问题的答案均可在数据中查询得到。

SQLite数据库.db

大小:1.46g
文件格式:sqllite db文件
文件数量:1
时间跨度:2019年-2021年
数据表说明:
◦ 基金基本信息
◦ 基金股票持仓明细
◦ 基金债券持仓明细
◦ 基金可转债持仓明细
◦ 基金日行情表
◦ A股票日行情表
◦ 港股票日行情表
◦ A股公司行业划分表
◦ 基金规模变动表
◦ 基金份额持有人结构

招股说明书

招股说明书 pdf源文件
大小:527MB
文件格式:pdf文件
文件数量:80

招股说明书 pdf解析后的txt文件
大小:44MB
文件格式:txt文件
文件数量:80

打卡任务

任务名称 所需技能
任务1:初始大模型与Agent
任务2:ChatGLM API使用 Python
任务3:数据库表内容解析 SQL
任务4:文本索引与答案检索 TFIDF、BM25
任务5:文本嵌入与向量检索 Embedding
任务6:文本多路召回与重排序 ReRank
任务7:文本问答Promopt优化 Python
任务8:问答意图识别(进阶方向) BERT/TFIDF
任务9:问答关键词提取(进阶方向) TextRank
任务10:扩展词与扩展查询(进阶方向) Word2Vec/BART
任务11:本地微调NL2SQL(进阶方向) ChatGLM

任务1:初始大模型与Agent

  • 任务说明:了解ChatGPT和Agent
  • 任务要求:
    • 能使用API进行对话
    • 理解大模型与Agent的关系
  • 打卡要求:理解Agent的原理

大模型介绍

ChatGPT是OpenAI开发的聊天生成预训练转换器,基于GPT-3.5和GPT-4架构。该模型通过强化学习训练,具有出色的语言生成能力。ChatGPT支持文字方式的交互,用户可以使用自然语言对话的方式与ChatGPT进行通信。API的引入使得开发者能够将ChatGPT整合到自己的应用中,实现自动文本生成、自动问答等功能。

GLM是智谱AI推出的新一代基座大模型,相比上一代有着显著提升的性能,逼近GPT-4。GLM支持更长的上下文(128k),具备强大的多模态能力,并且推理速度更快,支持更高的并发。GLM的API接口为开发者提供了在自己应用中利用GLM进行语言生成的机会,为多种领域的任务提供了新的解决方案。

虽然这两个大模型都非常有效,但我们希望所有的学习者都需要学会对应的API调用。如果在本地使用ChatGLM3-6B等开源模型,也可以完成类似功能,但整体效果肯定不如这些费用的API。在任务2中,为了方便所有同学参与,我们将使用在线的ChatGPT/GLM API进行开发。这为没有本地GPU资源的同学提供了更便捷的方式。但ChatGPT/GLM API 都是需要注册账号并付费才能进行使用,如果你没有账号请联系小助手,我们将想要参与学习的同学提供API token。

  • ChatGLM对话API(支持glm-3-turbo、glm-4)
import time
import jwt
import requests

# 实际KEY,过期时间
def generate_token(apikey: str, exp_seconds: int):
    try:
        id, secret = apikey.split(".")
    except Exception as e:
        raise Exception("invalid apikey", e)

    payload = {
        "api_key": id,
        "exp": int(round(time.time() * 1000)) + exp_seconds * 1000,
        "timestamp": int(round(time.time() * 1000)),
    }
    return jwt.encode(
        payload,
        secret,
        algorithm="HS256",
        headers={"alg": "HS256", "sign_type": "SIGN"},
    )

url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
headers = {
  'Content-Type': 'application/json',
  'Authorization': generate_token("填入Key", 1000)
}

data = {
    "model": "glm-3-turbo",
    "messages": [{"role": "user", "content": """你好"""}]
}

response = requests.post(url, headers=headers, json=data)

print("Status Code", response.status_code)
print("JSON Response ", response.json())

Agent介绍

大模型的Agent指的是以大型语言模型(如GPT-3、GPT-4等)为核心,构建的具有一定自主性和智能的软件实体。这些Agent能够执行多种任务,包括但不限于自然语言处理、决策制定、任务规划和执行等。

Agent通常具备以下特点:

  1. 上下文学习能力:大模型的Agent能够理解和处理复杂的上下文信息,这使得它们能够在对话中保持连贯性,理解用户的需求,并在此基础上做出响应。

  2. 推理能力:这些Agent能够进行逻辑推理,解决复杂问题,甚至在一定程度上模拟人类的思考过程。

  3. 任务规划与执行:Agent能够识别任务需求,规划执行步骤,并自主执行这些任务,这在自动化工作流程和提高效率方面非常有用。

  4. 自然语言交互:大模型的Agent能够以自然语言与用户进行交互,提供信息、解答问题或执行命令。

  5. 自主智能:尽管Agent的智能程度有限,但它们在特定领域内展现出一定程度的自主性,能够自主发现问题、设定目标、构思策略并执行任务。

  6. 多模态能力:一些Agent还能够处理和理解多种类型的数据,如文本、图像、声音等,这使得它们在多模态任务中表现出色。

大模型的Agent在多个领域都有应用,例如智能助手、客户服务、内容创作、教育、健康咨询等。随着技术的发展,这些Agent的能力正在不断增强,它们的应用范围也在不断扩大。然而,它们仍然面临着一些挑战,如处理幻觉问题、上下文容量限制等,这些都需要持续的研究和改进。

任务2:ChatGLM API使用

  • 任务说明:掌握API使用
  • 任务要求:
    • 能使用API进行调用
    • 使用API构建自己的Agent
  • 打卡要求:使用ChatGLM API分别完成对话,并构建一个自己的Agent

对话API说明

对话API是所有大模型的最常见的API,可以完成通用对话,也可以完成很多功能。但在进行调用时需要注意如下入参和参数返回结果。

  • 请求参数说明
参数 类型 必填 描述
messages Array 必填 包含对话的消息列表。
model String 必填 要使用的模型的ID。
frequency_penalty Number 或 null 可选 根据文本中已有令牌的频率对新令牌进行惩罚。取值范围在-2.0到2.0之间。
logit_bias Map 可选 修改指定令牌在完成中出现的可能性。接受一个将令牌映射到偏置值(-100到100)的JSON对象。
logprobs Boolean 或 null 可选 是否返回输出令牌的对数概率。
top_logprobs Integer 或 null 可选 如果 logprobs 设置为 true,则返回每个令牌位置上最有可能的令牌数,每个都带有关联的对数概率。
max_tokens Integer 或 null 可选 可以在聊天完成中生成的最大 令牌数
n Integer 或 null 可选 为每个输入消息生成的聊天完成选择的数量。
presence_penalty Number 或 null 可选 根据新令牌是否出现在到目前为止的文本中对其进行惩罚,增加模型谈论新主题的可能性。
seed Integer 或 null 可选 如果指定,系统将尽力进行确定性采样,以使具有相同 seed 和参数的重复请求应返回相同的结果。
stop String/Array 或 null 可选 API 将停止生成进一步的令牌的序列,最多可设置为 4 个。
stream Boolean 或 null 可选 如果设置,将发送部分消息增量,就像在 ChatGPT 中一样。令牌将作为数据仅 server-sent events 发送,一旦可用,流将以 data: [DONE] 消息终止。参考 Example Python code
temperature Number 或 null 可选 使用的采样温度,介于 0 和 2 之间。较高的值(如 0.8)会使输出更随机,而较低的值(如 0.2)会使其更集中和确定性。
top_p Number 或 null 可选 与温度采样的替代方法,称为核采样,其中模型考虑具有 top_p 概率质量的令牌的结果。因此,0.1 表示仅考虑构成前 10% 概率质量的令牌。
  • 返回结果字段
参数 类型 描述
id 字符串 用于唯一标识聊天完成的标识符。
choices 数组 聊天完成选择的列表。如果n大于1,则可以有多个选择。
created 整数 聊天完成创建的Unix时间戳(以秒为单位)。
model 字符串 用于聊天完成的模型。
system_fingerprint 字符串 此指纹表示模型运行时的后端配置。可与seed请求参数一起使用,了解可能影响确定性的后端更改。
usage 对象 完成请求的使用统计信息。
finish_reason 字符串 表示聊天完成的原因。可能的值包括"stop"(API返回了完整的聊天完成而没有受到任何限制),“length”(生成超过了max_tokens或对话超过了max context length),等等。

正则表达式Agent

提示工程(Prompt Engineering)是指在自然语言处理(NLP)中,特别是在使用预训练语言模型(如GPT-3)时,设计和优化输入(即提示)以引导模型产生期望的输出的过程。构建Agent时可能会用到提示工程的技术,提示工程是实现Agent功能的一部分。 提示工程的关键在于如何构造问题或指令,使得模型能够理解并生成准确、相关和有用的回答或内容。

使用GLM API完成正则表达式Agent:

  1. 编写prompt能写一个能识别首字母大写单词的正则。
  2. 编写prompt让ChatGPT写一个能识别首字母大写且字符个数小于10的正则。
  3. 编写prompt让ChatGPT写一个能识别单词末尾为标点符号的正则。
  4. 通过Python代码验证正则的有效性。
def regex_agent(question):
    prompt_template = '''你是一个专业的python的工程师,擅长编写各种的正则表达式。将下面的要求转换为正则匹配表达式,只需要输出表达式,不要有其他的输出。
{0}
    '''.format(question)

    return ask_glm(prompt_template)['choices'][0]['message']['content']

print(regex_agent("识别首字母大写单词的正则"))

任务3:数据库内容解析

  • 任务说明:掌握数据库解析步骤
  • 任务要求:
    • 能使用代码解析基金SQLite数据库
    • 能生成SQL并在SQLite数据库进行验证有效性
  • 打卡要求:构建初步的数据库Agent

数据库是存储、检索和管理数据的系统化方式。它允许用户和应用程序以结构化的形式存储大量数据,并能够高效地查询和操作这些数据。

  1. 数据模型
  • 数据模型定义了数据的结构和存储方式。常见的数据模型有关系模型(如 SQL 数据库)、文档模型(如 MongoDB)、键值存储模型(如 Redis)、图形模型(如 Neo4j)等。
  1. 数据库管理系统(DBMS)
  • DBMS 是软件,它提供了创建、管理和操作数据库所需的工具和服务。它允许用户执行各种操作,如插入、查询、更新和删除数据。流行的关系型数据库管理系统包括 MySQL、PostgreSQL、Oracle、SQL Server 和 SQLite。
  1. 数据表和字段
  • 在关系型数据库中,数据通常存储在表中,表由行(记录)和列(字段)组成。每个字段都有一个特定的数据类型,如整数、字符串、日期等。
  1. 索引
  • 索引是数据库中的一种结构,它可以加快数据检索速度。索引类似于书籍的目录,允许数据库系统快速定位到特定的数据记录。

对数据库进行解析通常涉及以下几个步骤:

  1. 连接数据库
    • 使用适当的数据库连接库(如 Python 的 sqlite3psycopg2MySQLdb 等)来建立与数据库的连接。
    • 提供必要的连接信息,如数据库地址、端口、用户名、密码等。
  2. 检索数据库模式
    • 使用 SQL 查询来获取数据库的模式信息,这可能包括表名、列名、数据类型、索引、外键约束等。
    • 常见的查询语句可能包括 DESCRIBE table_name;SHOW TABLES; 等。
  3. 分析查询结果
    • 对检索到的模式信息进行分析,以便理解数据库的结构。
    • 这可能涉及到解析 SQL 查询的结果集,提取表和列的元数据。
  4. 构建数据模型
    • 根据解析出的数据库模式,构建数据模型,这可以是对象关系映射(ORM)模型,也可以是其他形式的数据结构。
    • 数据模型应该能够反映出数据库中的关系和数据类型。
  5. 执行查询
    • 使用构建的数据模型来执行具体的 SQL 查询。
    • 根据需要,可以执行 SELECT、INSERT、UPDATE 或 DELETE 等操作。
  6. 处理查询结果
    • 分析和处理查询结果,这可能包括数据的清洗、转换和加载(ETL)。
    • 将结果转换为应用程序或用户所需的格式。

如果对单张表格进行解析,可以解析列名和类型。接着对每个字段执行一系列统计操作。这些操作包括计算唯一值的数量(distinct_count ),确定最频繁出现的值(mode ),统计缺失值(nan_count ),以及获取最大值(max )和最小值(min )。

'''数据库解析'''
from typing import Union
import traceback
from sqlalchemy import create_engine, inspect, func, select, Table, MetaData
import pandas as pd

class DBParser:
    '''DBParser'''
    def __init__(self, db_url:str) -> None:
        '''初始化
        db_url: 数据库链接地址
        '''

        # 判断数据库类型
        if 'sqlite' in db_url:
            self.db_type = 'sqlite'
        elif 'mysql' in db_url:
            self.db_type = 'mysql'

        # 链接数据库
        self.engine = create_engine(db_url, echo=False)
        self.conn = self.engine.connect()
        self.db_url = db_url

        # 查看表明
        self.inspector = inspect(self.engine)
        self.table_names = self.inspector.get_table_names()

        self._table_fields = {} # 数据表字段
        self.foreign_keys = [] # 数据库外键
        self._table_sample = {} # 数据表样例

        # 依次对每张表的字段进行统计
        for table_name in self.table_names:
            print("Table ->", table_name)
            self._table_fields[table_name] = {}

            # 累计外键
            self.foreign_keys += [
                {
                    'constrained_table': table_name,
                    'constrained_columns': x['constrained_columns'],
                    'referred_table': x['referred_table'],
                    'referred_columns': x['referred_columns'],
                } for x in self.inspector.get_foreign_keys(table_name)
            ]

            # 获取当前表的字段信息
            table_instance = Table(table_name, MetaData(), autoload=True, autoload_with=self.engine)
            table_columns = self.inspector.get_columns(table_name)
            self._table_fields[table_name] = {x['name']:x for x in table_columns}

            # 对当前字段进行统计
            for column_meta in table_columns:
                # 获取当前字段
                column_instance = getattr(table_instance.columns, column_meta['name'])

                # 统计unique
                query = select(func.count(func.distinct(column_instance)))
                distinct_count = self.conn.execute(query).fetchone()[0]
                self._table_fields[table_name][column_meta['name']]['distinct'] = distinct_count

                # 统计most frequency value
                field_type = self._table_fields[table_name][column_meta['name']]['type']
                field_type = str(field_type)
                if 'text' in field_type.lower() or 'char' in field_type.lower():
                    query = (
                        select([column_instance, func.count().label('count')])
                        .group_by(column_instance)
                        .order_by(func.count().desc())
                        .limit(1)
                    )
                    top1_value = self.conn.execute(query).fetchone()[0]
                    self._table_fields[table_name][column_meta['name']]['mode'] = top1_value

                # 统计missing个数
                query = select(func.count()).filter(column_instance == None)
                nan_count = self.conn.execute(query).fetchone()[0]
                self._table_fields[table_name][column_meta['name']]['nan_count'] = nan_count

                # 统计max
                query = select(func.max(column_instance))
                max_value = self.conn.execute(query).fetchone()[0]
                self._table_fields[table_name][column_meta['name']]['max'] = max_value

                # 统计min
                query = select(func.min(column_instance))
                min_value = self.conn.execute(query).fetchone()[0]
                self._table_fields[table_name][column_meta['name']]['min'] = min_value

                # 任意取值
                query = select(column_instance).limit(10)
                random_value = self.conn.execute(query).all()
                random_value = [x[0] for x in random_value]
                random_value = [str(x) for x in random_value if x is not None]
                random_value = list(set(random_value))
                self._table_fields[table_name][column_meta['name']]['random'] = random_value[:3]

            # 获取表样例(第一行)
            query = select([table_instance])
            self._table_sample[table_name] = pd.DataFrame([self.conn.execute(query).fetchone()])
            self._table_sample[table_name].columns = [x['name'] for x in table_columns]


    def get_table_fields(self, table_name) -> pd.DataFrame:
        '''获取表字段信息'''
        return pd.DataFrame.from_dict(self._table_fields[table_name]).T

    def get_data_relations(self) -> pd.DataFrame:
        '''获取数据库链接信息(主键和外键)'''
        return pd.DataFrame(self.foreign_keys)

    def get_table_sample(self, table_name) -> pd.DataFrame:
        '''获取数据表样例'''
        return self._table_sample[table_name]

    def check_sql(self, sql) -> Union[bool, str]:
        '''检查sql是否合理

        参数
            sql: 待执行句子

        返回: 是否可以运行 报错信息
        '''
        try:
            self.engine.execute(sql)
            return True, 'ok'
        except:
            err_msg = traceback.format_exc()
            return False, err_msg

    def execute_sql(self, sql) -> bool:
        '''运行SQL'''
        result = self.engine.execute(sql)
        return list(result)

parser = DBParser('sqlite:///./bs_challenge_financial_14b_dataset/dataset/博金杯比赛数据.db')
parser.get_table_fields("A股公司行业划分表")
def sql_agent(table_name, table_info, question):
    prompt_template = '''你是一个sql专家,基于已有的表格信息,请将下面的问题转换为sql查询语句。直接输出sql,不要输出其他内容。
表名称:{0}

表格信息:
{1}

待查询问题:{2}
'''.format(table_name, table_info, question)

    return ask_glm(prompt_template)['choices'][0]['message']['content']

sql_agent(
    "A股公司行业划分表",
    parser.get_table_fields("A股公司行业划分表").to_markdown(),
    '查询下总共有多少个股票'
)

任务4:文本索引与答案检索

  • 任务说明:文本文本索引的实现逻辑
  • 任务要求:
    • 理解倒排索引
    • 实现TFIDF和BM25的编码与检索
  • 打卡要求:使用TFIDF和BM25进行检索,使用question检索到答案的reference页面位置

文本检索流程

文本检索是一个多步骤的过程,其核心是构建倒排索引以实现高效的文本检索:

  • 步骤1(文本预处理):在文本预处理阶段,对原始文本进行清理和规范化,包括去除停用词、标点符号等噪声,并将文本统一转为小写。接着,采用词干化或词形还原等技术,将单词转换为基本形式,以减少词汇的多样性,为后续建立索引做准备。
  • 步骤2(文本索引):构建倒排索引是文本检索的关键步骤。通过对文档集合进行分词,得到每个文档的词项列表,并为每个词项构建倒排列表,记录包含该词项的文档及其位置信息。这种结构使得在查询时能够快速找到包含查询词的文档,为后续的文本检索奠定了基础。
  • 步骤3(文本检索):接下来是查询处理阶段,用户查询经过预处理后,与建立的倒排索引进行匹配。计算查询中每个词项的权重,并利用检索算法(如TFIDF或BM25)对文档进行排序,将相关性较高的文档排在前面。

在实际应用中,倒排索引的构建和维护需要考虑性能问题,采用一些优化技术来提高检索效率,如压缩倒排索引、分布式索引等。这些步骤共同构成了一个有序而逻辑完整的文本检索流程。

文本检索与语义检索

下面是文本检索和语义检索的区别和联系的表格形式:

文本检索 语义检索
定义 通过关键词或短语匹配文本数据的过程 强调理解查询与文本之间的深层语义关系
方法 基于关键词匹配,使用TFIDF、BM25等权重计算 使用NLP技术,如词嵌入、预训练的语言模型
特点 强调字面意义,关注表面文本的匹配 关注词语之间的关联、语境和含义
应用场景 大规模文本数据的快速匹配 对语义理解要求较高的场景
优势 处理速度较快,适用于大规模文本数据 能够处理一词多义、近义词等语义上的复杂情况
联系 结合使用,先使用文本检索筛选出候选文档,然后在这些文档上应用语义检索 可以利用语义模型提取关键词的上下文信息,提升检索效果

在一些场景中,文本检索和语义检索可以结合使用,以充分利用它们各自的优势。例如,可以先使用文本检索筛选出候选文档,然后在这些文档上应用语义检索来进一步提高检索的准确性。当然具体使用哪种检索方法,需要具体分析,在RAG中可以结合两种方法一起进行使用。

TFIDF

TFIDF(Term Frequency-Inverse Document Frequency)是一种用于信息检索和文本挖掘的常用权重计算方法,旨在衡量一个词项对于一个文档集合中某个文档的重要性。该方法结合了两个方面的信息:词项在文档中的频率(TF)和在整个文档集合中的逆文档频率(IDF)。

  1. 词项在文档中的频率(TF)
TF(t, d) = \frac{\text{词项t在文档d中出现的次数}}{\text{文档d中所有词项的总数}}

其中,$t$表示词项,$d$表示文档。TF表示了一个词项在文档中的相对频率,即在文档中出现的次数相对于文档总词项数的比例。

  1. 逆文档频率(IDF)
IDF(t) = \log\left(\frac{\text{文档集合中的文档总数}}{\text{包含词项t的文档数 + 1}}\right)

其中,$t$表示词项。IDF表示了一个词项在整个文档集合中的稀有程度,如果词项在许多文档中都出现,其IDF值较低,反之则较高。

  1. TFIDF的计算
TFIDF(t, d, D) = TF(t, d) \times IDF(t)

其中,$D$表示文档集合。TFIDF的最终值是将词项在文档中的频率和在整个文档集合中的逆文档频率相乘,这样可以得到一个更全面的评估,既考虑了在文档中的重要性,也考虑了在整个文档集合中的稀有性。

BM25

BM25Okapi是BM25算法的一种变体,它在信息检索中用于评估文档与查询之间的相关性。以下是BM25Okapi的原理和打分方式的概述:

  1. BM25Okapi的主要参数:
  • $k_1$:控制词项频率对分数的影响,通常设置为1.5。
  • $b$:控制文档长度对分数的影响,通常设置为0.75。
  • $epsilon$:用于防止逆文档频率(IDF)为负值的情况,通常设置为0.25。
  1. 打分的计算过程:

BM25Okapi的打分过程基于以下三个因素:词项在文档中的频率(TF)、文档的长度(doc_len)以及逆文档频率(IDF)。

  • TF(词项在文档中的频率)
  • IDF(逆文档频率)
  • 文档长度(doc_len)

文档长度对分数的影响通过 b 控制。文档长度越长,该项的分数越小。BM25Okapi的打分公式综合考虑了以上三个因素,通过对每个词项的打分求和得到最终的文档与查询的相关性分数。

\text{score} = \sum_{q \in \text{query}} \left( \text{IDF}(q) \cdot \frac{q\_freq \cdot (k1 + 1)}{q\_freq + k1 \cdot (1 - b + b \cdot \frac{\text{doc\_len}}{\text{avgdl}})} \right)

其中,$\text{avgdl}$是文档集合中的平均文档长度。BM25Okapi通过合理调整参数,兼顾了词项频率、文档长度和逆文档频率,使得在信息检索任务中能够更准确地评估文档与查询之间的相关性,提高检索效果。

注意事项

  1. 实现非工业级别

    • 提供的TFIDF和BM25的实现并非工业级别,仅作为演示目的。在实际进行文本检索时,特别是在大规模数据集和生产环境中,应该使用专业的文本检索引擎工具,例如Elasticsearch,以确保高效、可扩展和内存友好的实现。
  2. 相似度计算的内存和数据量级考虑

    • 在实际应用中,对整个文本集合构建矩阵并进行相似度计算可能导致内存占用较大,尤其在大规模数据集情况下。建议考虑使用基于倒排索引等数据结构的文本检索引擎,以减小内存占用并提高检索效率。
  3. 停用词和单词筛选

    • 未对文本进行停用词筛选和额外的单词筛选。在实际应用中,建议进行停用词的去除,以排除常见但无实际意义的词汇,提高检索的准确性。同时,考虑引入领域专有的单词筛选,以过滤掉与任务无关的词汇,优化检索结果。
  4. PDF处理方式

    • 将PDF内每一页都当做一个文档进行处理。实际应用中,对于PDF文档,可以考虑使用专业的PDF文本提取工具,提取有意义的文本内容,而不是将每一页都当做独立的文档处理。这有助于更好地利用文档内部的语义信息。

任务5:文本嵌入与向量检索

  • 任务说明:对文本进行编码,并进行语义检索
  • 任务要求:
    • 加载文本编码模型
    • 对提问和文档进行编码,并进行检索
  • 打卡要求:加载三个编码模型,计算检索结果

语义检索流程

语义检索是通过词嵌入和句子嵌入等技术,将文本表示为语义丰富的向量。通过相似度计算和结果排序找到最相关的文档。用户查询经过自然语言处理处理,最终系统返回经过排序的相关文档,提供用户友好的信息展示。语义检索通过深度学习和自然语言处理技术,使得系统能够更准确地理解用户查询,提高检索的准确性和效果。

文本编码模型

文本编码模型对于语义检索的精度至关重要。目前,大多数语义检索系统采用预训练模型进行文本编码,其中最为常见的是基于BERT(Bidirectional Encoder Representations from Transformers)的模型,或者使用GPT(Generative Pre-trained Transformer)等。这些预训练模型通过在大规模语料上进行训练,能够捕捉词语和句子之间的复杂语义关系。选择合适的文本编码模型直接影响到得到的文本向量的有效性,进而影响检索的准确性和效果。

编码模型排行榜:https://huggingface.co/spaces/mteb/leaderboard

  • M3E
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('../hugging-face-model/moka-ai/m3e-small/')
  • BGE
model = SentenceTransformer('../hugging-face-model/BAAI/bge-small-zh-v1.5/')

# 剩余代码与M3E部分相同
  • BCEmbedding
model = SentenceTransformer("../hugging-face-model/maidalun1020/bce-embedding-base_v1", device='cuda')
model.max_seq_length = 512

# 剩余代码与M3E部分相同

文本切分方法

文本的长度是另一个关键因素,影响了文本编码的结果。短文本和长文本在编码成向量时可能表达不同的语义信息。即使两者包含相同的单词或有相似的语义,由于上下文的不同,得到的向量也会有所不同。因此,当在语义检索中使用短文本来检索长文本时,或者反之,可能导致一定的误差。针对文本长度的差异,有些系统采用截断或填充等方式处理,以保持一致的向量表示。

更多阅读资料:

名称 分割依据 描述
递归式分割器 一组用户定义的字符 递归地分割文本。递归分割文本的目的是尽量保持相关的文本段落相邻。这是开始文本分割的推荐方式。
HTML分割器 HTML特定字符 基于HTML特定字符进行文本分割。特别地,它会添加有关每个文本块来源的相关信息(基于HTML结构)。
Markdown分割器 Markdown特定字符 基于Markdown特定字符进行文本分割。特别地,它会添加有关每个文本块来源的相关信息(基于Markdown结构)。
代码分割器 代码(Python、JS)特定字符 基于特定于编码语言的字符进行文本分割。支持从15种不同的编程语言中选择。
Token分割器 Tokens 基于Token进行文本分割。存在一些不同的Token计量方法。
字符分割器 用户定义的字符 基于用户定义的字符进行文本分割。这是较为简单的分割方法之一。
语义分块器 句子 首先基于句子进行分割。然后,如果它们在语义上足够相似,就将相邻的句子组合在一起。

对于自然语言,可以推荐使用Token分割器,结合Chunk Size和Overlap Size可以得到不同的切分:

  • Chunk Size(块大小):表示将文本划分为较小块的大小。这是分割后每个独立文本块的长度或容量。块大小的选择取决于应用的需求和对文本结构的理解。

  • Overlap Size(重叠大小):指相邻两个文本块之间的重叠部分的大小。在切割文本时,通常希望保留一些上下文信息,重叠大小就是控制这种上下文保留的参数。

任务6:文本多路召回与重排序

  • 任务说明:实现多种文本编码和检索逻辑,并进行重排序
  • 任务要求:
    • 结合文本索引和向量检索结果
    • 加载重排序模型,对检索进行重排序
  • 打卡要求:完成多路召回与重排序,与任务5精度进行对比

多路召回逻辑

多路召回逻辑是在文本检索中常用的一种策略,其目的是通过多个召回路径(或方法)综合获取候选文档,以提高检索的全面性和准确性。单一的召回方法可能由于模型特性或数据特点而存在局限性,多路召回逻辑引入了多个召回路径,每个路径采用不同的召回方法。

  • 实现方法1:将BM25的检索结果 和 语义检索结果 按照排名进行加权
  • 实现方法2:按照段落、句子、页不同的角度进行语义编码进行检索,综合得到检索结果。

重排序逻辑(BM25 + BGE Rerank)

重排序逻辑是文本检索领域中一种重要的策略,主要用于优化原有文本检索方法返回的候选文档顺序,以提高最终的检索效果。在传统的文本检索方法中,往往采用打分的逻辑,如计算BERT嵌入向量之间的相似度。而重排序逻辑引入了更为复杂的文本交叉方法,通过特征交叉得到更进一步的打分,从而提高排序的准确性。

  • 重排序逻辑常常使用更为强大的模型,如交叉编码器(cross-encoder)模型。这类模型能够更好地理解文本之间的交叉关系,捕捉更复杂的语义信息。
  • 首先通过传统的嵌入模型获取初始的Top-k文档,然后使用重排序逻辑对这些文档进行重新排序。这样可以在保留初步筛选文档的基础上,更精确地排列它们的顺序。

任务7:文本问答Promopt优化

  • 任务说明:将检索结果结合问题构造promot,完成问答
  • 任务要求:
    • 构造prompt
    • 调用API进行问答
  • 打卡要求:完成RAG完整流程,并提交结果进行打分

任务8:问答意图识别(进阶方向)

  • 任务说明:使用文本相似度和prompt进行意图识别
  • 任务要求:
    • 计算提问与现有文档的相似度
    • 构造prompt完成意图识别
  • 打卡要求:完成RAG完整流程,并提交结果进行打分

通过这种方式,意图识别允许系统更加灵活地适应用户的多样化需求。它允许系统在不同的上下文中识别用户意图,从而提供更准确、定制的回答。这种方法的优势在于通过使用专门的模型来处理特定领域的问题,可以提高系统的准确性和用户体验。

文本相似度

  • 步骤1:提取用户提问的嵌入向量
  • 步骤2:提取文档所有的嵌入向量
  • 步骤3:判断提问向量与文档向量的最低相似度,结合相似度大小进行判断

Prompt意图识别

你是一个汽车维修和汽车销售的专家,请判断下面的提问是否与汽车使用相关。

{用户提问}

输出:相关 / 不相关

任务9:问答关键词提取(进阶方向)

  • 任务说明:对用户的提问提取关键词
  • 任务要求:
    • 计算提问与现有文档的相似度
    • 构造prompt完成意图识别
  • 打卡要求:完成RAG完整流程,并提交结果进行打分

文本关键词抽取是自然语言处理领域的一项重要任务,其目标是从给定的文本中提取出最具代表性和有意义的单词或短语。这些关键词通常反映了文本的主题、内容或重要信息。常见的步骤包括分词、词性标注、停用词移除、计算词语权重以及关键词抽取算法等过程。

方法1:IDF

  1. 分词(Tokenization): 将文本拆分为单词或短语。这一步骤将文本转换为基本的语言单元,为后续的处理做准备。

  2. 移除通用词(Stopword Removal): 剔除常见的停用词,如"and"、“the”、"is"等,这些词在文本中普遍出现但往往没有实际的信息价值。这样做可以减少噪音,使关键词更集中在文本的内容性词汇上。

  3. 计算逆文档频率(IDF): 对于每个单词,计算其逆文档频率。逆文档频率是一个衡量单词重要性的指标,它通过对整个文本集合中包含该词的文档数取倒数来计算。

  4. 计算TF-IDF得分: 对于每个单词,计算其TF-IDF得分,即词频(TF)与逆文档频率(IDF)的乘积。TF表示单词在当前文档中的出现频率。

  5. 排序和选取关键词: 根据计算得到的TF-IDF得分对单词进行排序,选择排名前几的单词作为关键词。排名越高的单词表示在当前文档中具有更高的重要性。

方法2:KeyBERT

https://github.com/MaartenGr/KeyBERT

  1. Embedding文本: 首先,KEYBERT使用预训练的BERT模型,例如distilbert-base-nli-mean-tokens,将输入的文本嵌入到一个高维的向量空间中。BERT模型能够学习丰富的语义表示,因此生成的向量能够捕捉文本的语义信息。

  2. 计算余弦相似度: 然后,KEYBERT计算文档中每个候选关键词或关键短语与整个文档之间的余弦相似度。余弦相似度是一种衡量两个向量之间夹角的度量,它在这里用于度量嵌入向量之间的相似性。

  3. 排序关键词: 最后,根据计算得到的余弦相似度值,KEYBERT将关键词或关键短语排序,从而形成最终的关键词列表。余弦相似度越高,表示关键词与文档的语义相似度越大,因此在排序中位置越靠前。

方法3:Prompt关键词提取

你是一个专业的文本理解专家,现在请你识别下面内容中的关键词,将关键词使用空格隔开:

{输入文本}

为了提高关键词提取过程的效率,可以采用一种优化策略。首先,将所有文档通过预训练的嵌入模型映射到向量空间中,生成它们的向量表示。接着,通过计算文档之间的相似性,使用余弦相似度等度量方法,将相似的文档聚合成一个文档聚类。在每个文档聚类中,选择一个代表性文档,利用关键词提取模型生成关键词。

任务10:扩展词与扩展查询(进阶方向)

查询改写(Query Rewriting,或称为查询扩展Query Expansion)。查询改写的应用方式是对原始Query拓展出与用户需求关联度高的改写词,多个改写词与用户搜索词一起做检索,从而用更好的表述,帮用户搜到更多符合要求的文本。

  • 语义拓展:主要是同义词、下位词以及常见的大小写数字和繁简转化等,例如“理发”、“剪发”、“造型”、“发艺”、“美发”、“剪头”等等。
  • 用户表达和商家表达上的Gap:非语言上的同义。如用户表述口语化“学吉他”,商户描述书面化“吉他培训”;用户输入不完全匹配商户名:“希尔顿大酒店”(商家更常见的描述为“希尔顿酒店”)。
  • 场景拓展:例如“摘草莓”在美团的搜索场景下,用户基于对平台的认知对应需求是“草莓园”。
  • 其他漏召回问题:部分的多字少字、纠错等问题,如“房屋扫”对应“家政保洁”的需求;理论上查询改写可以通过增加改写词解决所有漏召回问题,诸如“冬日四件套”包括“冰糖葫芦、烤地瓜、炒栗子、热奶茶”这类有时效性的网红概念,也可以通过改写进行解决。

阅读链接:

通过词向量找到同义词

在进行查询改写时,可以利用词向量等技术找到同义词,以建立更丰富的词汇关联,从而提升搜索的全面性和准确性。

通过大模型生成扩展句

你是一个汽车维修和汽车销售的专家,将用户的提问改为含义相近当不相同的句子:

{用户提问}

任务11:本地微调ChatGLM(进阶方向)

ChatGLM3 是智谱AI和清华大学 KEG 实验室联合发布的对话预训练模型,ChatGLM3-6B 是 ChatGLM3 系列中的开源模型。ChatGLM3-6B 的基础模型 ChatGLM3-6B-Base 采用了更多样的训练数据、更充分的训练步数和更合理的训练策略。ChatGLM3-6B 采用了全新设计的 Prompt 格式 ,除正常的多轮对话外。同时原生支持工具调用(Function Call)、代码执行(Code Interpreter)和 Agent 任务等复杂场景。

ChatGLM6B 本地对话Demo

>>> from transformers import AutoTokenizer, AutoModel
>>> tokenizer = AutoTokenizer.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True)
>>> model = AutoModel.from_pretrained("THUDM/chatglm3-6b", trust_remote_code=True, device='cuda')
>>> model = model.eval()
>>> response, history = model.chat(tokenizer, "你好", history=[])
>>> print(response)
你好👋!我是人工智能助手
ChatGLM3 - 6
B, 很高兴见到你, 欢迎问我任何问题。
>>> response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=history)
>>> print(response)
晚上睡不着可能会让你感到焦虑或不舒服, 但以下是一些可以帮助你入睡的方法:

1.制定规律的睡眠时间表: 保持规律的睡眠时间表可以帮助你建立健康的睡眠习惯, 使你更容易入睡。尽量在每天的相同时间上床, 并在同一时间起床。
2.创造一个舒适的睡眠环境: 确保睡眠环境舒适, 安静, 黑暗且温度适宜。可以使用舒适的床上用品, 并保持房间通风。
3.放松身心: 在睡前做些放松的活动, 例如泡个热水澡, 听些轻柔的音乐, 阅读一些有趣的书籍等, 有助于缓解紧张和焦虑, 使你更容易入睡。
4.避免饮用含有咖啡因的饮料: 咖啡因是一种刺激性物质, 会影响你的睡眠质量。尽量避免在睡前饮用含有咖啡因的饮料, 例如咖啡, 茶和可乐。
5.避免在床上做与睡眠无关的事情: 在床上做些与睡眠无关的事情, 例如看电影, 玩游戏或工作等, 可能会干扰你的睡眠。
6.尝试呼吸技巧: 深呼吸是一种放松技巧, 可以帮助你缓解紧张和焦虑, 使你更容易入睡。试着慢慢吸气, 保持几秒钟, 然后缓慢呼气。

如果这些方法无法帮助你入睡, 你可以考虑咨询医生或睡眠专家, 寻求进一步的建议。

ChatGLM3-6B 微调示例

https://github.com/THUDM/ChatGLM3/tree/main/finetune_chatmodel_demo

专业名词介绍

专业名词 描述
大型语言模型 在自然语言处理领域展示出强大生成能力的模型,如GPT系列。但其修改、微调或重新训练困难,成本高。
Prompt “Prompt”(提示)是指一种引导大型语言模型(LLM)生成特定文本的方法。或可以理解为输给大模型的输入文本。
幻觉(Hallucination) 在自然语言处理领域被定义为生成的内容与提供的源内容无关或不忠实,一种虚假的感知。
知识库问答(KBQA) 早期的对话系统方法,利用结构化的知识库进行自然语言问题的回答。知识库以三元组形式表示<主题,关系,对象>,存储在图数据库中。
RAG RAG是检索增强生成(Retrieval-augmented Generation)的缩写,是一种结合了大型语言模型的生成能力和检索系统的精确性的技术,用于提高生成内容的准确性、相关性和时效性。
倒排索引 倒排索引(Inverted Index)是一种数据结构,用于加速文本检索过程。它将文档中的词汇映射到出现该词汇的文档列表,从而实现根据词汇快速检索相关文档的目的。
文本嵌入 文本嵌入是将文本信息映射到高维向量空间的过程,使得具有语义相似性的文本在向量空间中距离较近。
文本相似度 文本相似度是衡量两段文本之间语义接近程度的度量。通过计算文本在嵌入空间中的相似性,可以评估它们在语义上的相似程度。
排序与重排序 在信息检索中,排序指的是将检索到的文档按照其与查询的相关性进行排序。重排序则是在排序后的结果基础上再次调整文档的顺序,以进一步提高与用户查询的匹配度。
2 个赞