知也无涯

吾生也有涯,而知也无涯,能学一点算一点…

  • 这大概是一个有趣、也略深刻的发现。

    Word Embedding是比较抽象的,但是这些抽象背后是一些“具象”的含义的,本文通过一些简单的计算(变换)来将Embedding的某些维度/属性具象化。具体的,本文展示了在Embedding空间中,找到一个代表“动物”属性的方向。感兴趣的话,可以通过这个简单的方法,找到你感兴趣的属性方向。

    TL;DR

    通常,在某个具体的Word Embedding实现中,先给出一组具有“共同属性”的词语,然后计算这组词语Embedding向量的平均方向,就可以代表这个“共同属性”。

    例如,找到一组“动物”,然后对这些词语的Embedding向量计算平均方向,那么这个方向就是“动物”这个属性的方向。

    概述

    如果你也尝试过去理解 Embedding 各个维度的含义的话,大概都听过这样一种说法:Embedding每个维度可以理解为这个词语的某种属性,例如,“性别属性”、“皇室相关度”等,这是最为经典的man - woman = king - queue的例子中的一些解释。

    当你真的拿到一个词语的 Embedding 的时候,它可能有768维,但是,似乎没有一个维度有上述的清晰的属性含义。而实际上,这些属性含义是确实存在的,只是这些属性方向并不存在于“标准基”的方向上。

    那如果存在,我们应该如何找到这个方向呢?本文展示并验证了一个非常简单的方法,让你快速找到某种属性的方向,并且进行一些验证。从而可以大大加深对于 Embedding 的理解。

    寻找某个关心的方向

    这里展示了以寻找“动物”属性方向为例,展示如何寻找并验证该方向。

    列出最具代表性的词语

    我们这样考虑这个问题,如果有一个方向表示一个词语的“动物”属性,那么这个方向会是哪个方向?这里以all-MiniLM-L6-v2模型提供的Sentence Embedding为例,我看看如何找到该Embedding所处的向量空间中最可能代表“动物”属性的方向是哪个?具体的方法描述如下:

    • 首先,找到被认为最典型的与“动物”属性相关的词语\( n \)个,这里取\( n=50 \)
    • 然后计算上述\( n \)个词语的平均方向 avg_vector,该方向则认为要寻找的方向

    这里,给出的50个动物如下:

    animals = [
        "tiger", "lion", "elephant", "giraffe", "zebra",
        "rhinoceros", "hippopotamus","crocodile", "monkey",
        "panda", "koala", "kangaroo","whale", "dolphin",
        "seal", "penguin", "shark", "snake", "lizard",
        "turtle", "frog", "butterfly", "bee", "ant", "eagle",
        "sparrow", "pigeon", "parrot", "owl", "duck", "chicken",
        "dog", "cat", "pig", "cow", "sheep", "horse", "donkey",
        "rabbit", "squirrel", "fox", "wolf", "bear", "deer",
        "hedgehog", "bat", "mouse", "chameleon", "snail", "jellyfish"
    ]

    计算Embedding的平均方向

    该平均方向,即为我们要寻找的“动物”属性方向。

    animals_embeddings = model.encode(animals)
    avg_animals_embeddings = np.mean(animals_embeddings, axis=0)

    验证该方向

    再选取两组词,一组认为是与“动物”非常相关的词,另一组则是与动物无关的词语。然后分别计算这两组词语在上述方向avg_vector的投影值。观察投影值,是否符合预期。

    这里选择的两组词语分别是:

    • 与动物非常相关的:”Camel”, “Gorilla”, “Cheetah”
    • 与动物无关的:”Dream”, “Chair”, “Mathematics”

    计算投影并可视化

    具体的程序如下:

    animals_words    = ["Camel", "Gorilla", "Cheetah"]
    un_animals_words = ["Dream", "Chair", "Mathematics"]
    
    for word_list in (animals_words,un_animals_words):
        projection_scores = np.dot(model.encode(word_list),
                                  avg_animals_embeddings)
        results.update({word: score for word,
                        score in zip(word_list, projection_scores)})
    
    for word, score in results.items():
        print(f"'{word}': {score:.4f}")
    print(np.round(avg_animals_embeddings[:10], 4))

    投影结果为:

    'Camel': 0.3887
    'Gorilla': 0.4186
    'Cheetah': 0.3797
    'Dream': 0.2450
    'Chair': 0.2823
    'Mathematics': 0.1972

    在实数轴上绘制上述两组词语的投影:

    非常明显的可以看到,上述的avg_vector方向某种程度上代表了一个词语的“动物”属性:即与动物属性相关的词语在该方向的投影大,无关的词语在该方向的投影小。

    原理解释

    概述

    事实上,一组词语Embedding的“平均向量”(centroids of word embeddings),则某种程度的代表这组词语的“语义中心”。如果这组词有某些共性,那么这个平均向量,则可能就是这个共性的代表。

    在上述的例子中,刻意地给出的一组词语都是“动物”名称。那么,这个“平均向量”则比较有可能代表了这个向量空间中的“动物”属性。

    数学推导

    这样考虑这个问题:现在给出的 \( n \) 个向量 \( \alpha_1, \dots , \alpha_n \),找出一个单位向量 \( \xi \) 使得 \( n \) 个向量在 \( \xi \) 向量方向上的投影值的和最大。

    这里取 \( \bar{\alpha} = \frac{\sum\limits_{i=1}^{n}\alpha_i}{n} \)

    目标函数 \( S = \sum\limits_{i=1}^{n}(\alpha_i \cdot \xi ) = \sum\limits_{i=1}^{n}(\alpha_i) \cdot \xi = n \bar{\alpha} \cdot \xi = n \| \bar{\alpha}\| \| \xi \| \cos\theta \)

    这里 \( n \)、\( \bar{\alpha} \)都是给定值,而 \( \| \xi \| = 1 \),所以这里 \( \cos\theta \) 取最大值时,上述的目标函数 \( S \) 取最大值。

    即:\( \theta = 0 \) 时, \( S \) 取最大值。即当 \( \xi \) 与 \( \bar{\alpha} \) 方向相同时,即 \( \xi = \frac{\bar{\alpha}}{\|\bar{\alpha}\|} \) ,所有向量的投影值的和最大。

    投影计算

    太久不碰线性代数了,对于基本运算都不是很熟悉了。向量 \( \alpha \) 在 \( \beta \) 方向上的投影长度,计算公式如下:

    $$ proj = \frac{\alpha \cdot \beta}{\|\beta\|} $$

    证明比较简单,这里不再赘述。

    向量的平均方向与主成分方向

    当给出一组向量,面对上述问题,比较容易联想到这组向量的“主成分分析”的第一个维度。那么,上述的平均向量和主成分分析的第一个维度有什么关系呢?回答是:没有太大的关系。

    可以看下面三个图:

    上述三个二维平面中的点的平均方向均为红色,即(1,1);但是PCA的第一方向则各有不同,有时候与平均向量相同、有时候垂直,有时候相交。总之是没什么关系。

    可以看到,平均向量时在当前的“基”下计算获得。而主方向分析的方向,则首先就与原点没有关系。

    更深层次的理解

    现在的Embedding算法,都是基于现实世界语料库训练而来,反应了人类认知中“语言”与现实世界的对应关系。而在人类的认知中,这个世界是有“维度”的,最为直白的例子就是:我们会将词语划分“褒义词”、“贬义词”。此外,可能还有:动物性、情感强烈度、词性等。那么,在人类认知中这种“认知”有多少个维度呢?这其实是未知的,而各种Embedding算法则是在尝试使用量化的方式描述这些维度。

    但是,在实际训练出的各种Embedding实现,例如一个768维的Embedding,其单位向量方向,不太可能是上述的人类“认知”维度。如果把训练出来的Embedding的单位向量记为:\( \alpha_1, \dots , \alpha_n \),而把人类认知的维度记为: \( \beta_1, \dots , \beta_n \) 。

    那么,则存在一个过渡矩阵 $T$,可以实现上述向量空间的变换。

    可是,现实世界没有那么理想。Embedding空间确实给出了一组正交基,但是人类认识却很难寻找这样的正交基,例如“动物”属性的词语,可能会带有“情感”属性,例如,“虎狼之词”等,都带有某种情感属性。

    虽然,认知很难找到正交的“基”,但是找到某个具体的属性方向,则可以使用本书的方法。这正是本文所描述方法的局限性和价值所在。

    补充说明

    • 本文中,所说的Word Embedding,通常是指Sentence Embedding中的Token Embedding。在这里,无需区分两者。
    • 实际的情况更加复杂,例如本文中的“动物”属性,只是这些词所代表的“动物”属性。什么是真正的“动物”属性,并不存在这样的精确概念。人类语言中的“动物”是一个抽象的,并没有数字化、数学化的精确定义。
    • 完整的实现代码,参考:embedding_research_01.py
  • DTCC 2025 的 AI 部分观察

    ·

    上一次写DTCC已经是15年前了(参考:DTCC关于MySQL的未来),今天又有写一点什么的冲动了。因为要“练摊”,所以也只能是“部分”观察。

    AI 到底会如何改变数据库领域

    这次会场上,对于“佰晟智算”和“银联”在AI方向的一些探索关注的多一些,其他时间,则主要是在NineData展台“练摊”。说说一些感受吧。“佰晟科技”主要聚焦于国产化数据库的优化、监控管理等方向,最为亮眼的创新在于,将大模型的能力与“运维知识”深度的结合起来。白鳝很早就在给产品做市场预热了,所以,在产品推出比较短的时间能,就快速的获得了一些早期的种子客户。

    “银联”则是在内部的数据库管理上,做了很多的探索。银联有非常强的自主研发能力,也做了很多的智能化探索,包括Text2SQL、SQL的性能诊断等。Text2SQL很多的企业做了探索,但由于表名、列名的识别对于大模型来说,是非常困难的(一方面由于列可能非常多、而且命名比较简短),会让大模型出现非常强的幻觉问题,这使得在复杂OLTP场景,Text2SQL依旧难以胜任。但对于一些较为简单的场景,例如比较比较少,表、列都使用非常规范的时候,对于部分开发者,依旧有帮助。也注意到,有很多的企业在BI或者“取数”、“报表”场景,做了非常多的探索。

    在SQL优化的方向,AI的能力,已经得到了开发者比较一致的认可,大模型虽然可能会给出一些不太实用的建议,但是“正确”的优化建议,也总是在大模型给出的建议列表之中。这对于,DBA渐少的时代,对开发者来说,确实非常友好。

    NineData 在这个方向上,也做了很多探索,从最早发布ChatDBA以来,后续持续在“DDL转换”、“SQL 优化”、“Text2SQL”、“国产化转换”等方向去尝试,这些功能随着基础大模型的增强,以及辅助以各种优化,确实可以让开发者的数据库管理变得简单一些。

    Memobase

    还听了一场 Memobase 的分享,是一个关于大模型“memroy”的产品。创始人非常技术,整个介绍听下来,如果稍不留神,甚至不知道演讲者是来介绍 Memobase 的,而是把业界的“memroy”产品以及相关的技术栈介绍了清楚。本以为这可能是一个略微冷门的话题,但从现场的问答环节来看,开发者们在这个方向上有很多的问题要解决。

    这个是一个依旧在快速发展的方向。无论是Memobase还是Mem0,这类产品,与数据或者数据库的关系比较有趣的。简单的数据存储,即便是多模态存储,是无法简单的解决此类问题的。此类问题,当前,依旧是比较偏场景化的解决方案,例如,当天讨论的最多的,包括问答机器人、智能对话陪伴等。这些具体的场景下,产品需要深入到场景之中,才能解决开发者的问题。至于存储方案,可能不是当前最为紧迫的问题,所以存储上,可以用S3、也可以用“EloqData”、也可以考虑类似于MongoDB等其他的产品。

    Memobase 比较强调自己在 Latency 方面的优势,但是,目前来看,在一个Chat的场景下,多个百毫秒的Latency,似乎并不是问题。

    之所以,这里比较强调多模态的存储,主要在于:在这个场景中,通常会使用如下的方案,包括,使用 Graph 存储一些关联关系,例如一个人的朋友、“属性”等信息;还会大量使用json存储诸如profile、conversation 历史等信息。此外,这类方案,与RAG类似,也非常依赖注入Embedding、bm25等相似搜索,用于处理历史消息等。

    总得来说,是一个场景化的,混合的存储方案,去应对业务场景。

    创新与迭代是唯一出路

    这次在现场也与很多朋友讨论了 AI 对数据库从业者(不限于)的影响,大家也都有着类似的看法,如果你不是做基础大模型的,那么,基本上,如果你能够更好的使用 AI,那么就有可能开发者出更好的产品;如果你的产品,能够更好的使用AI的能力,你的产品可能会在市场上有更强的竞争力。

    对于开发者来说,确实应该更加积极、甚至激进的去拥抱 AI 技术。LLM从出圈到现在,一共也就两三年时间,所以,“大家的起点都一样,不要犹豫,往前跑就可以了”。

    这个说法在当初ChatGPT刚出来时,Google也有类似的论断类似:““我们没有护城河,OpenAI也没有(We Have No Moat, And Neither Does OpenAI)”。事实上,经过也就两年的时间,Google Gemini 的能力、体验与市场,已经逐步在赶上ChatGPT。

    另一方面,现在整个社会最多的风投资金、最聪明的人都聚集到了这个领域,这个领域的发展和变化,可以说是“日新月异”,这个领域一定会出现很多新的商业模式和企业。但如果,跑得晚了,后面的追赶会更加吃力。从最近的Zack如此大价钱的挖掘 AI 人才,也可能看出,即便是,最头部的厂商,在这个势头下,也是非常焦虑的。

    向量数据库是AI还是数据库

    向量存储在搜索在多个AI场景都有这广泛的使用,这次大会上,包括腾讯、华为、中兴、Oracle等厂商都介绍自己自己在这个方向的探索,包括海量存储下的性能优化、标量与向量混合查询的性能、面相RAG常见的效果优化、高效的向量缓存方案等。

    最后

    DTCC 是一年一度的数据库领域朋友聚会,非常开心。因为要“练摊”的原因,错过了很多的主题分享,今年的DTCC就简单记录如上。

  • 大语言模型非常非常强大,但也有一些弱点。例如,需要精确推理与计算的场景、实时数据(如天气)获取等。MCP则是对这类外部能力扩展的一个接口,让大模型/Agent都能够便捷的接入外部工具解决此类问题。

    本文通过演示创建一个外部计算24点的程序(MCP Server),让Cursor Agent访问LLM时具备快速的24点计算能力,从而帮助开发者快速了解如何构建一个MCP Server。该24点计算的MCP Server也已经在🤗 Hugging Face对外发布,你也可以接入你的Cursor(或其他MCP Host)进行测试(参考本文小结:在Cursor中配置MCP工具)[1]

    创建 MCP Server

    现在各种工具框架已经把MCP入门构建的门槛降低非常低了,这里将使用Gradio构建一个24点计算的程序,并以MCP Server的方式提供给各MCP Host(本文是Cursor)使用,同时将该MCP Server发布在🤗 Hugging FaceSpace上,以供其他人测试和使用。

    创建24点计算的MCP Server

    使用Gradio创建MCP的代码如下:

    #
    # A project for mcp learning by orczhou
    #
    from solve_24_game import solve_24_game
    import gradio as gr
    
    def gradio_interface(a, b, c, d):
        return solve_24_game([a, b, c, d])
    
    # Create the Gradio interface
    demo = gr.Interface(
        fn=gradio_interface,
        title="solve the 24 game/puzzle",
        inputs=[
            gr.Number(label="Number 1", value=1),
            gr.Number(label="Number 2", value=2),
            gr.Number(label="Number 3", value=3),
            gr.Number(label="Number 4", value=4),
        ],
        outputs="text",
        flagging_mode="never",
        description="Solves the 24-point game. Given a list of four numbers, it attempts to find a mathematical expression using addition, subtraction, multiplication, and division that evaluates to 24. Each number must be used exactly once.",
        theme=gr.themes.Ocean()
    )
    
    # Launch the interface and MCP server
    if __name__ == "__main__":
        demo.launch(mcp_server=True)

    Gradio 不仅可以快速构建可视化的交互界面(通常用于机器学习领域),还可以非常简单的构建起MCP Server,并将其托管于🤗 Hugging Face

        demo.launch(mcp_server=True)

    在启动时,新增mcp_server=True即可以同时启动一个与此界面“相同”的MCP Server

    左侧的代码首先创建了一个如下Web服务:

    在创建了上述的Web服务的同时,Gradio还会同时创建了一个如下Endpoint的MCP Server:

    https://orczhou-solve-24-game.hf.space/gradio_api/mcp/sse

    如果是本地运行,Endpoint则可能是:

    http://127.0.0.1:7860/gradio_api/mcp/

    这里解决24点问题的代码存储在文件solve_24_game.py中,代码参考:solve_24_game.py。如何解决24点问题并不是本文的重点,这里不做详述。

    本地运行该MCP Server

    在本地,则只需要使用python3 app.py命令即可运行:

     python3 app.py
    * Running on local URL:  http://127.0.0.1:7860
    * To create a public link, set `share=True` in `launch()`.
    
    🔨 Launching MCP server:
    ** Streamable HTTP URL: http://127.0.0.1:7860/gradio_api/mcp/
    * [Deprecated] SSE URL: http://127.0.0.1:7860/gradio_api/mcp/sse

    在Hugging Face上发布 MCP Server

    🤗 Hugging Face上可以非常方便的创建并托管简单的MCP Server。详细的介绍可以参考:Spaces as MCP servers。这里演示如何进行操作。

    前提要求

    1. 首先,你要有一个🤗 Hugging Face的账号,注册即可
    2. (可选)可能还需要进行充值与信用卡绑定
    3. 准备好🤗 Hugging FaceToken,并配置好权限

    通常,Hugging Face运行程序的资源是需要付费的。但是也有部分免费资源,例如MCP托管的时候,提供了一个免费的CPU Basic(2 vCPU 16 GB RAM)的免费资源(当前免费,未来也可能是计费的)。

    在本地代码向Space上推送的时候,则需要通过Token的方式进行认证和权限管理。

    创建 Space

    进入🤗 Hugging Face,进入Space,点击“+ New Space”创建新的Space,则进入右侧的创建表单。

    注意到,在Space Hardware选项中,这里的CPU Basic 2 vCPU 16GB是免费规格,这里用作个人测试故选择免费。

    此外,这里选择了Gradio模板进行创建。

    可以看到,这里🤗 Hugging Face把相关操作的入门门槛降到了非常低的程度,对初学者非常友好。

    提交代码

    在完成创建后,可以使用 git想仓库中提交代码,Space则会根据代码架构,完全自动化的构建一个MCP Server向公网提供服务。

    代码提交可以参考如下命令:

    git clone https://huggingface.co/spaces/orczhou/solve_24_game
    cd solve_24_game
    git add solve_24_game.py app.py
    git commit -m "mcp for 24 point game"
    git remote add origin https://huggingface.co/spaces/orczhou/solve_24_game
    git push -u origin main

    补充说明:首先在Hugging Face托管则需要把程序命名为app.py,此外,还需要编写一个requirements.txt文件说明Pythone程序需要的一些模块,这里仅需要gradio[mcp]

    cat requirements.txt
    gradio[mcp]

    调试

    这里使用Hugging FaceGradio构建的MCP服务,可以非常方便的使用可视化的界面进行查看服务,例如,在这里可以通过,如下的URL来进行查看该服务是否正常:solve the 24 game/puzzle

    此外,可以通过curl命令进行调试,以确认MCP服务是否正常:

    curl -X POST \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {}
      }' \
      -L \
      https://orczhou-solve-24-game.hf.space/gradio_api/mcp/
    data: {
      "jsonrpc":"2.0",
      "id":1,
      "result":{
        "tools":[{
          "name":"solve_24_game_gradio_interface",
          "description":"",
          "inputSchema":{
            "type":"object",
            "properties":{
              "a":{"type":"number", "default":1 },
              "b":{"type":"number", "default":2 },
              "c":{"type":"number", "default":3 },
              "d":{"type":"number", "default":4 }
            }
          }
    }]}}

    在Cursor中配置该MCP服务

    配置新的MCP服务

    CursorSettings->Tools & Integrations中找到MCP Tools,就可以通过New MCP Server处编辑mcp.json,从而增加MCP Tools

    具体的配置,参考如下:

    {
      "mcpServers": {
        "solve_24_game": {
          "type": "sse",
          "url": "https://orczhou-solve-24-game.hf.space/gradio_api/mcp/sse"
        }
      }
    } 

    查看Cursor中MCP配置状态

    首次测试和使用MCP时,还是比较容易出错的。可以通过上述的MCP Tools处(右图)可以查看MCP的状态,已经该MCP中可用的工具。

    在Cursor中使用该MCP服务

    Agent模式下进行对话,Cursor背后的大模型就可以使用该MCP的能力。为了避免Agent生成代码(Cursor的Agent总是倾向于生成代码解决问题),故使用如下提示词提问:

    不要生成任何代码,使用工具计算如下24点问题: 2 5 5 10”。

    返回的结果如右图。可以看到,大模型拿到返回的结果(5-(2/10))*5后,进行了必要的解释与回答。MCP详细的调用过程如下,包括了使用的参数、返回的结果:

    在我的Cursor上使用该MCP

    因为这里创建的MCP Server是运行在公网环境,故在你本地的Cursor或者其他MCP Host上也可以配置和使用该MCP服务,配置的方法参考上述小结“配置新的MCP服务”,这里不再赘述。

    其他

    更换Gradio的默认配色主题

    可以使用 theme=gr.themes.Ocean()选项更改主题选项:

    # Create the Gradio interface
    demo = gr.Interface(
        ...
        theme=gr.themes.Ocean()
    )

    更多的配色主题可以参考:Theming Guide

    最后

    大模型说:“我不需要帮助,愚蠢的人类!”

  • MySQL的向量处理现状

    向量数据库或者说向量处理是,个人认为,最为重要数据库AI能力。目前,各个数据库都在围绕着向量数据库构建更为丰富的LLM/AI相关功能。而MySQL 9最为重要的特性之一就是新增了向量处理能力。当前的版本主要包括了:(a) 向量数据类型;(b) 简单的向量处理函数。其中部分向量处理函数放在了MySQL的企业版或云版本中。

    因为当前MySQL 9系列的版本均为创新版(并不是稳定版),所以相关功能还会不断的迭代和发展。期待未来做出更多丰富功能:(a) 新增向量相似性搜索功能;(b) 并将完整的向量处理能力放到社区版中。后续依旧会持续关注这部分的产品能力。

    本文测试环境为Oracle Cloud,你可以参考在 Oracle 云上免费测试数据库[3]文中创建一个免费的MySQL实例进行测试。

    MySQL中的向量数据类型

    向量数据存储

    MySQL 9中新增了数据类型vector用来存储向量数据[1],简单的使用方式如下:

    create table vector_t01 (
        id int,
        s_v_01 vector(390),
        s_v_02 vector(390)
    );

    这里表示s_v_01、s_v_02均为390维的向量,每个维度在MySQL中使用4 bytes的单精度浮点类型存储。

    写入向量数据

    为了测试使用方便,这里使用string_to_vector对向量进行转换并进行存储:

    insert into vector_t01 values (1,string_to_vector('[1,2,3]'),string_to_vector('[4,5,6]'));

    查询向量数据

    使用 VECTOR_TO_STRING

    使用 SELECT 直接查询向量数据的话,则返回的是二进制形式,可以使用函数VECTOR_TO_STRING做一次转换:

    select id,VECTOR_TO_STRING(s_v_01),VECTOR_TO_STRING(s_v_02) FROM vector_t01;
    +------+---------------------------------------+---------------------------------------+
    | id   | VECTOR_TO_STRING(s_v_01)              | VECTOR_TO_STRING(s_v_02)              |
    +------+---------------------------------------+---------------------------------------+
    |    1 | [1.00000e+00,2.00000e+00,3.00000e+00] | [4.00000e+00,5.00000e+00,6.00000e+00] |
    +------+---------------------------------------+---------------------------------------+

    如果不使用VECTOR_TO_STRING则返回的是底层的二进制存储内容:

    select id,s_v_01,s_v_02 FROM vector_t01;
    +------+--------------+--------------+
    | id   | s_v_01       | s_v_02       |
    +------+--------------+--------------+
    |    1 |   �?   @  @@  |   �@  �@  �@    |
    +------+--------------+--------------+

    十六进制查询

    如果使用十六进制展示,则有:

    select id,hex(s_v_01),hex(s_v_02) FROM vector_t01;
    +------+--------------------------+--------------------------+
    | id   | hex(s_v_01)              | hex(s_v_02)              |
    +------+--------------------------+--------------------------+
    |    1 | 0000803F0000004000004040 | 000080400000A0400000C040 |
    +------+--------------------------+--------------------------+

    这里的0000803F0000004000004040一共是3个bytes,每个byte表示一个分量,例如:0000803F表示第一个分量,即为单精度浮点型的1.0(感兴趣的可以尝试做个转换,这里不再详述)。

    计算向量距离

    目前,MySQL 支持最为常见的“距离”的计算,具体包括:点积(默认)、欧式距离、余弦距离的计算:

    -- s_v_01: (1,2,3)
    -- s_v_01: (4,5,6)
    SELECT 
      DISTANCE(s_v_01,s_v_02,"DOT") as dis_dot,
      DISTANCE(s_v_01,s_v_02,"COSINE") as dis_cos,
      DISTANCE(s_v_01,s_v_02,"EUCLIDEAN") as dis_ecu
    FROM vector_t01;
    
    +---------+----------------------+-------------------+
    | dis_dot | dis_cos              | dis_ecu           |
    +---------+----------------------+-------------------+
    |      32 | 0.025368213653564453 | 5.196152210235596 |
    +---------+----------------------+-------------------+

    需要注意的是,目前该距离计算函数(DISTANCE[2])仅在Oracle Cloud或MySQL企业版本中提供。后续还将持续关注MySQL所提供的向量产品能力、以及其他GenAI相关功能。

    参考链接

  • 理解 NumPy 中的高维数组

    ·

    在机器学习中大量的使用NumPy作为其基础的数据结构,ndarrayNumPy的核心数据对象。对于ndarray高维数组的一个非常容易产生的误解是,使用数学中的矩阵(或者叫“行列式”)概念去尝试理解更高维的场景,或者使用更高维空间去理解,这样都会导致难以较好的理解更高维(5或6维)的数组。本文使用较为直观的示例和可视化的展示,更为“标准”(文档推荐的)的方式去理解ndarray的更高维数组。更多详细内容,可以参考阅读:

    问题

    在机器学习中,经常要对多维的数组做各种操作。对高维数组建立更好的直觉理解,则有利于去理解这些操作。例如,我们考虑右侧的代码,想一想该代码的输出是什么?

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (考虑输出是什么)

    要回答这个问题,则需要建立对于多维数组结构的理解。

    文档中对于高维数组理解的建议

    NumPy: the absolute basics for beginners中有如下一段话:

    It is familiar practice in mathematics to refer to elements of a matrix by the row index first and the column index second. This happens to be true for two-dimensional arrays, but a better mental model is to think of the column index as coming last and the row index as second to last. This generalizes to arrays with any number of dimensions.

    “矩阵”是“线性代数”的主要研究对象,一个\( m \times n \)的矩阵即是一个平面上的\( m \)行\( m \)列的行列式。一种常见的向高维扩展的思考方式是,会将三维数组扩展为三维空间中的数组。但,这样的扩展,非常不利于去理解更高维的数组。这里提到的方案是这样:“a better mental model is to think of the column index as coming last and the row index as second to last.”。

    这种理解,也是本文的核心,概况如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

    从 4×5 的数组开始

    先通过直观的书写表达,看看这样的数组应该是怎样的。

    一般的矩阵(行列式表示):

    \begin{bmatrix}
    0 & 1 & 2 & 3 & 4 \\
    5 & 6 & 7 & 8 & 9 \\
    10 & 11 & 12 & 13 & 14 \\
    15 & 16 & 17 & 18 & 19 \\
    \end{bmatrix}

    本文推荐的理解方式:

    \[
    \begin{bmatrix}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19]
    \end{bmatrix}
    \]

    numpy的输出:

    >>> np.arange(20).reshape(4,5)
    array([[ 0,  1,  2,  3,  4],
           [ 5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14],
           [15, 16, 17, 18, 19]])

    如果按照“矩阵”思想去理解这个矩阵很简单。但这里,我们重新按照上述的原则去理解这个数组。即:

    • 最后一个维度(即第二个维度),该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即第一个维度,该维度的长度是4,将其理解为维度

    形式上,这与一般的矩阵,是完全一致的。只是,思维方式,反过来了。

    再考虑 3x4x5 的数组

    这个数组已经不能用简单的平面表示了,这里使用了符合上述描述的形式描述,“剩余的维度,则是通过层的方式去构建”,则有:

    本文推荐的理解方式:

    \[
    \begin{array}{r c}
    \text{Layer 1:} &
    \left[
    \begin{array}{c}
    [0 & 1 & 2 & 3 & 4] \\
    [5 & 6 & 7 & 8 & 9] \\
    [10 & 11 & 12 & 13 & 14] \\
    [15 & 16 & 17 & 18 & 19] \\
    \end{array}
    \right]
    \\
    \text{Layer 2:} &
    \left[
    \begin{array}{c}
    [20 & 21 & 22 & 23 & 24] \\
    [25 & 26 & 27 & 28 & 29] \\
    [30 & 31 & 32 & 33 & 34] \\
    [35 & 36 & 37 & 38 & 39] \\
    \end{array}
    \right]
    \\
    \text{Layer 3:} &
    \left[
    \begin{array}{c}
    [40 & 41 & 42 & 43 & 44] \\
    [45 & 46 & 47 & 48 & 49] \\
    [50 & 51 & 52 & 53 & 54] \\
    [55 & 56 & 57 & 58 & 59] \\
    \end{array}
    \right]
    \end{array}
    \]

    >>> np.arange(60).reshape(3,4,5)
    array([[[ 0,  1,  2,  3,  4],
            [ 5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14],
            [15, 16, 17, 18, 19]],
    
           [[20, 21, 22, 23, 24],
            [25, 26, 27, 28, 29],
            [30, 31, 32, 33, 34],
            [35, 36, 37, 38, 39]],
    
           [[40, 41, 42, 43, 44],
            [45, 46, 47, 48, 49],
            [50, 51, 52, 53, 54],
            [55, 56, 57, 58, 59]]])

    这时,矩阵的想法就不太好用了。这里继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第三个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第二个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度(第一个维度)该维度长度是3,将其理解为行列式前面的

    循着这样的思考模式,不断地叠加更多的“层”,就可以理解更高维度的数组了。

    考虑 2x3x4x5 的数组

    这里先试用“层”的思维,可视化的表示该数组如下:

    \[
    \left[
    \begin{array}{c}
    \text{Layer}^{(0)}_1
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [000 & 001 & 002 & 003 & 004] \\
    [005 & 006 & 007 & 008 & 009] \\
    [010 & 011 & 012 & 013 & 014] \\
    [015 & 016 & 017 & 018 & 019] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [020 & 021 & 022 & 023 & 024] \\
    [025 & 026 & 027 & 028 & 029] \\
    [030 & 031 & 032 & 033 & 034] \\
    [035 & 036 & 037 & 038 & 039] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [040 & 041 & 042 & 043 & 044] \\
    [045 & 046 & 047 & 048 & 049] \\
    [050 & 051 & 052 & 053 & 054] \\
    [055 & 056 & 057 & 058 & 059] \\
    \end{array}
    \right]
    \end{array}
    \right] \\
    \text{Layer}^{(0)}_2
    \left[
    \begin{array}{c}
    \text{Layer}^{(1)}_1
    \left[
    \begin{array}{c}
    [060 & 061 & 062 & 063 & 064] \\
    [065 & 066 & 067 & 068 & 069] \\
    [070 & 071 & 072 & 073 & 074] \\
    [075 & 076 & 077 & 078 & 079] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_2
    \left[
    \begin{array}{c}
    [080 & 081 & 082 & 083 & 084] \\
    [085 & 086 & 087 & 088 & 089] \\
    [090 & 091 & 092 & 093 & 094] \\
    [095 & 096 & 097 & 098 & 099] \\
    \end{array}
    \right] \\
    \text{Layer}^{(1)}_3
    \left[
    \begin{array}{c}
    [100 & 101 & 102 & 103 & 104] \\
    [105 & 106 & 107 & 108 & 109] \\
    [110 & 111 & 112 & 113 & 114] \\
    [115 & 116 & 117 & 118 & 119] \\
    \end{array}
    \right]
    \end{array}
    \right]
    \end{array}
    \right]
    \]

    >>> np.arange(120).reshape(2,3,4,5)
    array([[[[  0,   1,   2,   3,   4],
             [  5,   6,   7,   8,   9],
             [ 10,  11,  12,  13,  14],
             [ 15,  16,  17,  18,  19]],
    
            [[ 20,  21,  22,  23,  24],
             [ 25,  26,  27,  28,  29],
             [ 30,  31,  32,  33,  34],
             [ 35,  36,  37,  38,  39]],
    
            [[ 40,  41,  42,  43,  44],
             [ 45,  46,  47,  48,  49],
             [ 50,  51,  52,  53,  54],
             [ 55,  56,  57,  58,  59]]],
    
    
           [[[ 60,  61,  62,  63,  64],
             [ 65,  66,  67,  68,  69],
             [ 70,  71,  72,  73,  74],
             [ 75,  76,  77,  78,  79]],
    
            [[ 80,  81,  82,  83,  84],
             [ 85,  86,  87,  88,  89],
             [ 90,  91,  92,  93,  94],
             [ 95,  96,  97,  98,  99]],
    
            [[100, 101, 102, 103, 104],
             [105, 106, 107, 108, 109],
             [110, 111, 112, 113, 114],
             [115, 116, 117, 118, 119]]]])

    继续按照上面的原则,考虑:

    • 最后一个维度,即这里的第四个维度,该维度的长度是5,将其理解为维度
    • 倒数第二个维度,即这里第三个维度,该维度的长度是4,将其理解为维度
    • 倒数第三个维度,即第二个维度,该维度的长度是3,将其理解为行列式前面的
    • 倒数第四个维度,即第一个维度,该维度的长度是2,将其理解为行列式前面的层的层,也就是上述的“Layer”

    使用这样的模式,就可以将一个多维数组的表示平面化。并且注意到,这与ndarray输出的形式是几乎完全一致的。

    回到前面的问题

    有了上面的可视化展示以及上面逐步的介绍,应该可以更容易理解前面NumPy: the absolute basics for beginners所提到的直觉“a better mental model is to think of the column index as coming last and the row index as second to last”。

    有了这个直觉,我们再来考虑最前面提到的问题:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape

    最内层的列,就是最后的维度长度,这里是 1,所以就是 ? x 1;该列所对应的行数,就是倒数第二个维度的长度,这里做如下的格式化,可以看到有两行,所以这是一个? x 2 x 1的数组;再向上看一层,共有两个该2x1的数组,故,该数组的shape时:2x2x1

    [
      [
        [1],
        [2]
      ],
      [
        [3],
        [4]
      ]
    ]

    再确认最后的输出:

    >>> import numpy as np
    >>> np.array([[[1],[2]],[[3],[4]]]).shape
    (2, 2, 1)

    最后

    这种理解的核心即是“a better mental model is to think of the column index as coming last and the row index as second to last”,简单概括如下:

    • 总是将最后一个维度理解为列维度
    • 总是将倒数第二个维度理解为行维度
    • 剩余的维度,则是通过层的方式去构建

  • Oracle LogMiner 使用

    ·

    Oracle官方文档Using LogMiner to Analyze Redo Log Files[2]中,对该功能有详细的介绍,包括了LogMiner的配置与使用、数据过滤、补充日志(Supplemental Logging)、使用示例等。

    Oracle 何时引入的LogMiner?

    自 1999 年发布 Oracle 8i 的时候,正式引入 LogMiner 功能(参考:Redo Log Analysis Using LogMiner[1])。该功能支持以SQL的形式分析redo中的数据,最初考虑的应用场景,主要还是偏于故障恢复、异常诊断、审计等,但是该功能的潜力很大,现在已经逐步成为Oracle CDC的主流方案之一。

    目前已经有很多的集成/同步工具都使用LogMiner进行变化数据获取,虽然,目前官方依旧不推荐这么做,文档中的原文如下:“Note:LogMiner is intended for use as a debugging tool, to extract information from the redo logs to solve problems. It is not intended to be used for any third party replication of data in a production environment.”

    根据经验来看,LogMiner用于数据集成并没有什么太大的问题。

    打开补充日志

    ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;

    查看归档日志

    SQL> SELECT name FROM v$archived_log ORDER BY FIRST_TIME DESC FETCH FIRST 3 ROWS ONLY;
    
    NAME
    ----------------------------------------------------------------------------------------------------
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_761_n0mbccbd_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_760_n0mbcc62_.arc
    /u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_759_n0m6tdfp_.arc

    添加需要解析的日志文件

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/PATH_TO_YOUR_ARCHIVE/o1_mf_1_761_n0mbccbd_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    例如,实际的SQL可能是如下的样子:

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u03/app/oracle/fast_recovery_area/ORCLCDB/archivelog/2025_04_24/o1_mf_1_955_n0mwc01v_.arc',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /
    
    o1_mf_1_955_n0mwc01v_.arc
    o1_mf_1_909_n0mv4sc3_.arc
    o1_mf_1_908_n0mv4ohs_.arc

    启动LogMiner

    启动时,可以带不同的参数以指定LogMiner不同的行为。

    使用在线数据字典启动
    EXECUTE DBMS_LOGMNR.START_LOGMNR( -
       OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);
    使用日志中的数据字典启动
    EXECUTE DBMS_LOGMNR_D.BUILD( -
        OPTIONS => DBMS_LOGMNR_D.STORE_IN_REDO_LOGS);

    获取LogMiner中的变更数据

    获取变更数据
    SELECT 
      SCN,
      TIMESTAMP,
      OPERATION,
      SQL_REDO,
      SQL_UNDO,
      SEG_OWNER,
      TABLE_NAME,
      USERNAME
    FROM 
      V$LOGMNR_CONTENTS
    WHERE 
        OPERATION IN ('INSERT', 'UPDATE', 'DELETE')
    --  AND SEG_OWNER = 'TEST_USER'
        AND (TABLE_NAME = 'T2' OR TABLE_NAME = 't2')
    ORDER BY TIMESTAMP DESC
    FETCH FIRST 3 ROWS ONLY;

    退出 LogMiner

    EXECUTE DBMS_LOGMNR.END_LOGMNR();

    解析未归档的 Redo 日志

    除了解析归档之外,LogMiner 可以直接解析当前正在使用的 redo 文件。先根据小节“获取当前正在使用的redo文件”中的SQL获取当前正在使用的 redo 文件,然后在添加日志文件时,像上述添加归档一样添加即可。

    BEGIN
      DBMS_LOGMNR.ADD_LOGFILE(
        LOGFILENAME => '/u04/app/oracle/redo/redo003.log',
        OPTIONS => DBMS_LOGMNR.NEW
      );
    END;
    /

    使用上述(小结“获取变更数据”)的SQL可以获得如下的输出:

           SCN TIMESTAMP OPERATION	  SQL_REDO
    ---------- --------- ------------ ---------------------------------------------
       3175619 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('31');
       3175846 24-APR-25 INSERT       insert into "SYS"."T2"("ID") values ('32');

    一些常见的 SQL

    获取归档日志

    获取最新的ARCHIVE LOG的列表,以及对应的SCN号范围:

    SELECT 
      FIRST_CHANGE#,
      TO_CHAR(FIRST_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NEXT_CHANGE# ,
      TO_CHAR(NEXT_TIME, 'YYYY-MM-DD HH24:MI:SS'),
      NAME 
    FROM  
      V$ARCHIVED_LOG 
    ORDER BY NEXT_CHANGE# DESC 
    FETCH FIRST 3 ROWS ONLY;
    强制切换归档日志文件
    ALTER SYSTEM SWITCH LOGFILE;
    获取当前正在使用的redo文件
    SELECT A.GROUP#,A.MEMBER, B.STATUS
    FROM   V$LOGFILE A
    JOIN   V$LOG B ON A.GROUP# = B.GROUP#
    WHERE  B.STATUS = 'CURRENT'; 

    参考