码农戏码

新生代农民工的自我修养

0%

一个SaaS,但凡涉及客户现场部署场景,打包必然就会面临标准化与定制化两种路径

总有各种各样的原因来强迫架构的演化,不以架构师的意志转移

标准化

首先不管是架构师还是公司,都希望只有一个标准化。

标准化能把高昂的研发成本摊薄到海量用户上,就可以降低边际成本,实现规模化盈利。

开发团队会预埋大量的if-else或策略模式,把所有可能的变化都抽象成配置文件(如YAML、JSON)。用户通过修改配置来“定制”,但这些配置不改变程序本质,只是给同一套代码换了不同的“皮肤”或“流程顺序”

定制化

定制化的核心技术是 “模块化”与“插件化”
开发者会设计出清晰的“内核”和“外挂”机制。基础功能标准化,但关键的业务逻辑做成可替换的“插件”或“微服务”。定制时,只需重写或新增一个插件模块,而不用动整个底层引擎。

其中插件化,类似DDD中供应商模式。

model层有接口,而具体实现在各个“供应商”

比如发消息,有微信、钉钉、飞书。

总体原则:构建时用 Maven 控制依赖是否打进包;运行时用 Spring 条件装配隔离缺失的类

Maven 层面控制依赖打包

1、把功能依赖声明为 optional

common/pom.xml 中,把仅部分客户使用的依赖声明为 optional:

1
2
3
4
5
<dependency>  
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<optional>true</optional>
</dependency>

作用:common 模块本身可以编译,但依赖不会传递到 main 模块

<optional>true</optional> 控制的是依赖传递性,不是“当前模块自己不打这个 jar”

含义:common 模块自己可以使用这个依赖来编译和运行,但不会把这个依赖传递给依赖 common 的其他模块

假设模块结构:

1
main  -->  depends on  -->  common  -->  javacv-platform (optional=true)
情况 不加 <optional> 加了 <optional>true</optional>
common 编译时 能使用 javacv-platform 能使用 javacv-platform
main 是否会自动继承 javacv-platform 不会
如果 start 想要这个依赖 什么都不用做 必须在 main/pom.xml 里显式声明

main 作为 Spring Boot 模块:默认会把所有非 optional 的传递依赖打进 fat jar。如果 common 里的 javacv-platform<optional>true</optional>main 不会自动继承它,所以最终 fat jar 里不会包含 javacv-platform,除非 start 显式引入。

main/pom.xml 按 profile 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<profiles>
<profile>
<id>customer-a</id>
<dependencies>

<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
</dependency>
</dependencies>
</profile>

<profile>
<id>customer-b</id>

</profile>
</profiles>

<optional><scope>provided</scope> 都能“不传递给下游”,但它们对打包的影响完全不同。关键在于:

  • <optional>true</optional> 只是一个传递性开关,不改变依赖本身的 scope(默认仍是 compile)。它影响的是“下游模块会不会自动继承这个依赖”,不影响“声明模块自己运行时/打包时带不带它”。
  • <scope>provided</scope> 改变的是依赖的生命周期范围:编译/测试可见,运行时不可见,普通 jar/war 打包时不带它。但 Spring Boot 的 spring-boot-maven-plugin 默认会把 provided 也打进 fat jar(它只默认排除 system scope),所以在本项目的 Spring Boot fat jar 里,provided 并不能自动把依赖排除掉。

Spring 官方文档明确说明:

By default, both the repackage and run goals will include any provided dependencies that are defined in the project. A Spring Boot project should consider provided dependencies as container dependencies that are required to run the application.

spring-boot-maven-pluginrepackagerun 目标默认就会包含所有 provided 依赖,官方将其视为“运行所需的容器依赖”。

正确排除方式

若需排除特定 provided 依赖,需在插件中显式配置:

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>com.aliyun.odps</groupId>
<artifactId>odps-sdk-core</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>

Spring 层面隔离缺失依赖

1
2
3
4
5
6
7
8
9
@Service
@ConditionalOnClass(FFmpegFrameRecorder.class)
public class JavaCvVideoEncoderService implements VideoEncoderService {

@Override
public Map<String, Integer> encodeToMp4(String imageDir, String outputPath, double fps, int bitrate) {

}
}

关键点:

  • @ConditionalOnClass(FFmpegFrameRecorder.class):classpath 中没有 javacv 类时,该 Bean 不会被创建,也不会触发类加载。

调用方注入 Optional

1
2
3
4
5
6
7
8
@RestController
public class ReviseDataController {

private final Optional<VideoEncoderService> videoEncoder;

public ReviseDataController(Optional<VideoEncoderService> videoEncoder) {
this.videoEncoder = videoEncoder;
}

论起大运法
凡起大运,俱从所生之日,阳男阴女顺行,数至未来节。阳女阴男逆行,数已过去节,但折除三日为一岁。

阳男阴女顺运,

假如甲子年,甲己之年丙作首,正月建丙寅,初一日立春后一日生男,顺数至二月节惊蛰,且得三十日,起十岁运,顺行丁卯。

如乙丑年,乙庚之岁成为头,正月起戊寅,初一立春,十八日生女,顺数至二月惊蛰节止,得四三十二日,起四岁运,顺行己卯。余皆仿此。阴男阳女逆运。

假如乙丑年,乙庚之岁戊为头,正月起戊寅,初一日立春后十五日生男。逆数至初一立春节止,得五三十五日,起五岁运,运行丁丑。

如甲子年,甲己之年丙作首,正月雨寅,初一立春后十日生女,逆数至初一日立春止,得九日,三三单九日,起三岁运,逆行乙丑。

余皆仿此。若多一日,减一日;少一日,增一日。

【眉批:如初一日午时生,若卯时立春,便作正月;若寅时生,未交春,还上年十二月节推命。如正月初一子时生,上四刻仍作上年十二月夜子时生,下四刻方作正月初一子时推。正谓神仙难断夜子时。】

多一日,减一日;少一日,增一日

示例

  • 如果到节气有 15 天

  • 15 ÷ 3 = 5

  • 起运年龄 = 5 岁

  • 如果到节气有 17 天

  • 17 余 2,少一日,增一日 → 18 天

  • 18 ÷ 3 = 6

  • 起运年龄 = 6 岁

  • 如果到节气有 16 天

  • 16 余 1,多一日,减一日 → 15 天

  • 15 ÷ 3 = 5

  • 起运年龄 = 5 岁

经典示例

甲子年,甲己之年丙作首,正月丙寅,初一立春后十日生女,逆数至初一日立春止,得九日,三三单九日,起三岁运,逆行乙丑。

  • 年柱:甲子(阳年)
  • 性别:女 → 阴男阳女逆行
  • 月柱:丙寅
  • 出生日距上一个节(立春)共 9 天
  • 9 ÷ 3 = 3
  • 第一步大运:从月柱丙寅逆行一步 → 乙丑
  • 故:3 岁起运,逆行乙丑

距离关心两个向量“隔多远”,余弦相似度关心两个向量“方向像不像”。
它们让模型能做搜索、推荐、分类和聚类

欧氏距离

欧氏距离(Euclidean Distance)其实就是我们日常生活中最熟悉的 “直线距离”。它衡量的是两点之间的最短路径长度。

余弦相似度

余弦相似度通过计算两个向量之间的夹角余弦值来衡量它们的相似性。

余弦值越接近1,夹角越小,说明两个向量越相似;
余弦值越接近-1,夹角越大,说明两个向量越不相似;
余弦值为0时,两个向量正交,表示它们之间没有相关性。

在计算余弦相似度时,我们只关注向量之间的夹角,而不考虑它们的长度
因此,即使两个向量的长度不同,只要它们同向,余弦相似度依旧能够达到 1。

余弦公式

  • A⋅B 表示向量 A 和向量 B 的点积。
  • ||A|| 代表向量 A 的长度(模)。
  • ||B|| 代表向量 B 的长度(模)。
  • θ 是两个向量之间的夹角

为什么公式中有模

上面提到向量的夹角,与向量长度无关,但公式里面为什么会出现模呢?还得去计算模?

为什么不能直接“不看模长”?

向量的“模”(长度)和“方向”是捆绑存储在一个坐标里的。比如向量 A = (3, 4):

  • 它的模长是 5 (3平方+4平方​)再开根
  • 它的方向是 53.1°

在计算机眼里,它就是一个数组 [3, 4]模和方向是混在一起的二进制数字
你没法写一行代码说“把模去掉,只留方向”,因为数字 3 既贡献了方向,也贡献了长度。

如果你直接拿 [3, 4] 去算点积,结果里必然混着长度的影响。所以,我们必须通过数学运算把“长度”单独提取出来。

为什么公式里要“先乘起来,再除掉”?

第一步(分子:计算点积):先让两个向量发生完整的交互。此时,A的长度、B的长度、以及它们的夹角,三者全部被混在一起,揉成了一个数值(点积)。
第二步(分母:除以模长的乘积):既然点积里混杂了“A的长度 × B的长度 × 夹角”,那我就在外面强行除以“A的长度 × B的长度”。

相当于:

(∣∣A∣∣×∣∣B∣∣×cosθ)​ / ∣∣A∣∣×∣∣B∣∣ = cosθ

为什么必须“计算模长”,而不是“忽略模长”?

因为你无法凭空忽略模长,你只能用模长来抵消自身

  • 如果你不计算分母 ∣∣A∣∣×∣∣B∣∣,你得到的就是带长度的点积,大向量永远欺负小向量。
  • 你计算分母,不是为了“看重”模长,恰恰是为了 “利用”模长去“消除”模长

为什么不使用Math.cos(θ )

在编程中不调用自带的 Math.cos()函数,实在令人惊讶。
这是因为该函数是直接计算出夹角的余弦值,而无需先求出夹角本身。
如果使用 Math.cos(),我们就需要先用诸如 Math.acos()之类的反三角函数来求出角度,这不仅没有必要,而且计算成本更高。

为什么余弦相似度与词义有相关性

早期的实践归纳(启发式):
在早期的信息检索(如 TF-IDF、LSA 潜在语义分析)中,工程师们发现:如果把文档变成词频向量,用余弦相似度来匹配,效果出奇的好。这完全是实践归纳出来的经验法则。人们发现高维空间中的“几何邻近性”恰好对应了人类的“语义相关性”。

后期的理论证明(统计学习理论):
到了 Word2Vec 和深度学习时代,这不再仅仅是经验。学者们(如 Mikolov, Bengio 等)通过分布式假设(Distributional Hypothesis)——“上下文相似的词,语义相似”——给出了理论解释。

随后,大量的数学证明表明:通过最大化预测上下文的概率(如 Skip-gram 的负采样目标函数),模型实际上是在隐式地分解词共现矩阵(如 PMI 矩阵)。

这就从理论上证明了:为什么优化这个损失函数,最终会让相似的词在向量空间中靠近。

Reference

How does cosine similarity work?

在AI里面,常有 “把高维向量用于AI的搜索、推荐和语义理解” 的表达

这儿的高维向量是什么意思?

低维是我们生活的世界,直观但容量有限;
高维是 AI 的“大脑”,反直觉但拥有极其庞大的信息存储和分类能力。

AI 的本质,其实就是用低维的输入,在内部构建高维的向量空间,最后再输出低维的结果(比如一个分类标签或一句回答)

而且“高维”和“低维”其实是一个相对的概念,并没有一条绝对的界线。在数学和机器学习中,它们有着不同的特性和应用场景。

什么是低维向量?(通常指 1~3 维)

低维向量就是我们日常生活中最熟悉的几何空间:

  • 1维(标量/直线):比如你的体重(70kg),或者温度计上的刻度。
  • 2维(平面):比如地图上的经纬度 (x,y)(x,y) ,或者一张黑白照片的像素矩阵。
  • 3维(立体空间):比如你所在房间的长宽高 (x,y,z)(x,y,z) ,或者一个 3D 游戏模型。

特点:在低维空间中,人类的直觉是完全适用的。两点之间直线最短,空间看起来很“空旷”,距离和角度的计算非常直观。

什么是高维向量?(通常指几十、几百到上万维)

当向量的维度 dd 变得很大时(比如 d≥100d≥100 ),我们就进入了高维空间。

  • AI 中的 Embedding:比如 OpenAI 的文本向量通常是 1536 维,图像模型(如 CLIP)的向量可能是 768 维。
  • 推荐系统:如果用 1000 个标签来描述用户偏好,这就是一个 1000 维向量。
  • 原始数据:一张 1000x1000 的彩色图片,如果直接拉平,就是一个 3,000,000 维的向量。

特点:高维空间完全反人类直觉,这也是为什么它听起来很神秘的原因。

高维空间有哪些“反直觉”的神奇现象?

高维向量之所以强大,正是因为低维做不到的事情,高维能做到:

  • 维度灾难(Curse of Dimensionality)
    在 3D 空间里,数据很容易挤在一起。但在 1000 维空间里,空间大到难以想象,所有的数据点都变得极其稀疏。这导致在高维空间中,所有点之间的距离都变得差不多远。这就是为什么高维搜索不能直接算距离,必须用特殊的算法(如 HNSW、FAISS)。
  • 空间变得像“刺猬”
    在低维空间,两个随机向量的夹角可能是各种度数;但在高维空间,两个随机向量的夹角几乎总是接近 90 度(正交)。这意味着高维空间里有无数个互相垂直的方向,模型可以把“苹果”和“香蕉”放在完全正交的方向上,互不干扰地存储海量信息。
  • 更容易被“切开”(线性可分)
    在 2D 平面上,如果红点和蓝点像甜甜圈一样嵌套在一起,你画一条直线是切不开它们的。但在 3D 空间中,你可以用一个平面把它们切开。维度越高,数据越容易被一条直线/一个超平面分开。这就是为什么 AI 分类任务在高维向量上做得特别好。

高维和低维的相互转换(降维)

既然高维这么好,为什么我们还要关心低维?因为高维太占内存,且人类看不懂。
所以,AI 领域有一个极其重要的分支叫降维(Dimensionality Reduction),比如 PCA(主成分分析)或 t-SNE。

  • 目的:把 1000 维的向量,压缩成 2 维或 3 维。
  • 代价:压缩必然丢失信息,但好的算法能保留最核心的“结构”(比如聚类关系)。
  • 应用:我们在论文里看到的那些非常漂亮的、五颜六色的散点图(展示 AI 把猫和狗分开了),其实都是把高维向量降维到 2D 平面后画出来的。

标量

标量就是一个数字

只有大小概念,没有方向的概念。通过一个具体的数值就能表达完整。比如:重量、温度、长度、提及、时间、热量等都数据标量

向量

向量是数学和物理学中的一个基本概念,表示既有大小又有方向的量。

与标量(只有大小没有方向)不同

向量主要有2个维度:大小、方向

在数学里,向量可以用一串数字表示,比如 [3, 4]。这串数字可以代表空间中的一个点或者一个方向

矩阵

矩阵是二维的数字表格

矩阵可以看作是”多个向量的堆叠”:

3×4 的矩阵 = 3 个长度为 4 的行向量
或者 = 4 个长度为 3 的列向量

矩阵乘法

可乘条件‌:设 A 为 m×n 矩阵,B 为 n×p 矩阵,只有 A 的列数 (n) 等于 B 的行数 (n) 才能相乘
结果维度‌:乘积 C=AB 是 m×p 矩阵,保留第一个矩阵的行数和第二个矩阵的列数 。
元素计算‌:C 的第 i 行第 j 列元素等于 A 的第 i 行与 B 的第 j 列对应元素乘积之和。【点积(Dot Product)】

矩阵乘法的维度规则:

1
[A, B] × [B, C] = [A, C]

中间的维度必须相同(都是 B),结果取两边的维度。

3行2列矩阵与2行3列矩阵的相乘

举例:

矩阵加法

只有同行数同列数的矩阵才可以相加,结果也是同样尺寸的矩阵,其中的每个元素都是相加矩阵对应位置的元素之和

矩阵加法的一个例子,运算法则比较简单

Transformer中

标量:学习率、温度参数
向量:一个 token 的 embedding
矩阵:一批 token 的 embedding、权重矩阵

现在玩股票有个坏习惯,每次不是自己看盘复盘得出结论,而是看微博,去看别人的观点做结论

表多了时间就不准

而且关注人多,观点也是乱七八糟,脑子就乱了

每个人都说自己预测得多准确

想着来写个统计数据吧

核心思想:根据发微博的时间,交叉验证一下,后面几天博主所阐述的观点的正确率

只统计了短期的,1日 + 5日 + 10日

所以会有偏差,比如某人说未来一个月都是涨势,但可能前10日都在跌,后20日都在涨,那这个统计就不准确了

创建一个Skill,来实现这个需求

博主预测股市正确率看板(1日 + 5日 + 10日后验)


name: weibo-stock-monitor
description: |
微博股票观点监控技能。从指定微博分组抓取财经博主帖子,AI提取股票观点(做多/做空/中性),并用真实行情数据交叉验证博主观点与市场趋势的一致性。输出 Markdown 日报 + HTML 暗色主题看板。

触发场景:用户要求监控微博财经博主观点、查看某分组股票讨论、做微博观点日报等。
示例输入:「监控微博股票观点」「财经分组日报」「微博博主多空分析」「抓取股票分组帖子」

前一段时间,Java服务端经常崩溃,每天基本需要重启两次

之前出现过内存泄漏,但这次通过grafana查看,各项指标看着也正常,内存没有大幅上升

不过在查看整个宿主机时,发现整台机器内存一直在高位运行

所以依然怀疑是内存因素

措施一:把Java应用配置上JVM参数,-Xmx,限制一下整个应用内存使用,Java部署在Docker里面,Docker没有做内存限制
默认使用的内存似乎是32G,不管在128G内存的宿主机上,还是1024G的宿主机上,都差不多是这个数字

当参加了JVM参数,-Xms25G 时,重启次数会减少,三天重启一次

说明,内存因素的确是存在的,但根本原因是什么?

在服务器上发现了一份文件:hs_err_pid781.log

日志是怎么产生的

hs_err_pid<进程号>.log 是 JVM 的默认安全机制——当 JVM 遇到无法恢复的致命错误时,会在进程崩溃前自动生成此文件,相当于 JVM 的”临终遗言”。

触发条件:

错误类型 是否生成 hs_err log 说明
SIGSEGV / SIGBUS(native 内存越界) ✅ 是 JVM 内部访问了非法内存地址
Native OOM(malloc 失败) ✅ 是 malloc 失败
JVM 内部断言失败 ✅ 是 JVM 自身逻辑检测到不一致
Java 堆 OOM(OutOfMemoryError) ❌ 否 这是可恢复的 Java 异常,应用可 catch
StackOverflowError ❌ 否 可恢复

不生成的情况: 可恢复的 Java 层异常(堆 OOM、StackOverflow)不会触发此文件,因为 JVM 还能继续运行。只有 JVM 自身内部崩溃(native 层无法继续)才会生成。

1
2
3
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (malloc) failed to allocate 32744 bytes for ChunkPool::allocate
# Out of Memory Error (arena.cpp:81), pid=1, tid=37

JVM 在执行 BulkRevokeBias(批量撤销偏向锁)操作时,需要通过 Arena 内存池分配 32KB 的原生内存,malloc 调用失败,JVM 无法继续运行,直接崩溃。

日志结构

通过分析,文本的整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────┐
│ 1. 头部报错信息 (~27 行, 0.02%) │ ← 崩溃类型与建议
│ 2. SUMMARY (~7 行, 0.01%) │ ← 命令行/主机/时间
│ 3. THREAD (崩溃线程) (~28 行, 0.02%) │ ← 崩溃线程调用栈
│ 4. PROCESS (线程列表) (~40050行, 36.6%) │ ← 31952个线程详情 ⚠️最大段
│ 5. VM state / Mutex (~5 行, <0.01%) │ ← safepoint状态
│ 6. Heap (堆内存详情) (~3400行, 3.1%) │ ← G1 region逐个列出
│ 7. 运行时事件记录 (~70 行, 0.06%) │ ← 各种事件最近20条
│ 8. Dynamic libraries (~65734行,59.9%) │ ← native库内存映射 ⚠️最大段
│ 9. VM Arguments (~50 行, 0.04%) │ ← JVM启动参数
│ 10. SYSTEM (~180 行, 0.16%) │ ← OS/CPU/内存信息
│ 11. vm_info (~3 行, <0.01%) │ ← JVM版本
└─────────────────────────────────────────────┘

最终可以发现

项目 结论
崩溃类型 Native OOM(malloc 失败),不是 Java 堆 OOM
直接触发 BulkRevokeBias 操作时 Arena 内存分配失败
根本原因 线程泄漏——31952个线程消耗了约31GB原生内存
加剧因素 CompressedOops Zero Based 模式限制了原生内存地址空间
堆状态 使用率仅20.6%,堆完全正常,问题不在堆
系统状态 物理内存仅剩592MB,Swap已用9GB

一句话总结:线程泄漏导致原生内存耗尽,Java堆监控看不到这个问题,最终在偏向锁撤销操作时malloc失败导致JVM崩溃。

在日志的线程列表中,的确发现了大量的线程:

1
2
3
4
5
6
7
0x00007efdc0a00000,0x00007efdc0b00000)]
0x00007effeca46000 JavaThread "pool-3068-thread-3" [_thread_blocked, id=4218, stack(0x00007efdb9400000,0x00007efdb9500000)]
0x00007effeca47800 JavaThread "pool-3068-thread-4" [_thread_blocked, id=4219, stack(0x00007efdbbc00000,0x00007efdbbd00000)]
0x00007effec024800 JavaThread "pool-3068-thread-5" [_thread_blocked, id=4220, stack(0x00007efdb8e00000,0x00007efdb8f00000)]
0x00007effec026000 JavaThread "pool-3068-thread-6" [_thread_blocked, id=4221, stack(0x00007efdc0700000,0x00007efdc0800000)]
0x00007effec027000 JavaThread "pool-3068-thread-7" [_thread_blocked, id=4222, stack(0x00007eff30800000,0x00007eff30900000)]
0x00007effec028800 JavaThread "pool-3068-thread-8" [_thread_blocked, id=4223, stack(0x00007efdb8700000,0x00007efdb8800000)]

但这些线程是哪个线程池创建的,还得去排查代码。

这其实再一次印证了使用线程池规范:

关于线程池命名,阿里巴巴Java开发手册中有一条**【强制】**规约,原文表述如下:

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

手册将这条规约与“禁止用Executors创建线程池”放在同等重要的位置,均为强制要求。这足以说明命名规范在生产环境中的重要性。

遵循规范后,当程序出现问题(如死锁、CPU飙升)时,我们通常会使用jstack命令导出当前线程快照进行分析。一个有明确业务含义的线程名,能让开发者在成百上千条线程堆栈信息中,一眼定位到问题线程所属的业务模块,从而大大缩短故障排查时间。

这些规范都是前人的血泪史!

《从零开始构建智能体》第三章 大语言模型基础-读书笔记

语言模型 (Language Model, LM) 是自然语言处理的核心,其根本任务是计算一个词序列(即一个句子)出现的概率。一个好的语言模型能够告诉我们什么样的句子是通顺的、自然的。

在多智能体系统中,语言模型是智能体理解人类指令、生成回应的基础。


最初的 Transformer 模型是为端到端任务机器翻译而设计的。如图所示,它在宏观上遵循了一个经典的编码器-解码器 (Encoder-Decoder) 架构。

Transformer 是一种在自然语言处理(NLP)领域极具影响力的深度学习模型架构,最初由 Google 团队在 2017 年的论文《Attention Is All You Need》中提出。它彻底改变了 NLP 领域,成为了后来 BERT、GPT 等著名模型的基础。

以下是对图中各个部分的详细解析:

整体结构

该架构主要分为两大部分:

  1. 编码器 (Encoder) :任务是“理解”输入的整个句子。它会读取所有输入词元,最终为每个词元生成一个富含上下文信息的向量表示。
  2. 解码器 (Decoder) :任务是“生成”目标句子。它会参考自己已经生成的前文,并“咨询”编码器的理解结果,来生成下一个词。

这是一个典型的“编码器-解码器”架构,常用于机器翻译等序列到序列(Seq2Seq)的任务。

1. 输入部分 (Input)

  • Inputs (输入):这是模型的原始输入序列(例如一句话中的单词)。
  • Input Embedding (输入嵌入):将离散的单词索引转换为连续的向量表示,以便模型处理。
  • Positional Encoding (位置编码):因为 Transformer 不像循环神经网络(RNN)那样按顺序处理数据,它本身没有“顺序”的概念。位置编码被加到嵌入向量中,用来告诉模型单词在句子中的位置信息。

2. 编码器 (Encoder) - 左侧堆叠部分

编码器由 $N$ 个相同的层堆叠而成(图中的 $N\times$ 表示重复 $N$ 次,原论文中 $N=6$)。每一层包含两个主要子层:

  • 多头注意力机制 (Multi-Head Attention):这是 Transformer 的核心。它允许模型在处理一个词时,“关注”输入序列中的其他词。多头意味着模型会在不同的表示子空间中并行地执行多次注意力机制,从而捕捉更丰富的信息。
  • 前馈神经网络 (Feed Forward):这是一个简单的全连接层,对每个位置的向量进行相同的线性变换和激活函数处理。
  • Add & Norm (残差连接与层归一化):在每个子层周围都有残差连接(箭头绕过子层直接相加),然后进行层归一化。这有助于训练深层网络,防止梯度消失。

3. 解码器 (Decoder) - 右侧堆叠部分

解码器同样由 $N$ 个相同的层堆叠而成。每一层包含三个主要子层:

  • 掩码多头注意力机制 (Masked Multi-Head Attention):这与编码器中的注意力机制类似,但增加了一个“掩码”。这是为了在训练时防止模型在预测当前位置的词时“偷看”后面的词(即未来的信息)。
  • 多头注意力机制 (Multi-Head Attention):这一层接收来自解码器上一层的输出,以及来自编码器最终输出的信息。它让解码器能够关注输入序列中的相关部分(例如在翻译时关注源语言句子中的对应单词)。
  • 前馈神经网络 (Feed Forward):与编码器中的结构相同。
  • 同样,每个子层周围也有 Add & Norm

4. 输出部分 (Output)

  • Linear (线性层):解码器的输出经过一个线性变换(全连接层),将向量映射到一个高维空间,其维度等于词汇表的大小。
  • Softmax:将线性层的输出转换为概率分布。每个值代表对应词汇表中单词被选为下一个输出词的概率。
  • Output Probabilities (输出概率):最终模型预测的下一个词的概率。

总结

这张图清晰地展示了 Transformer 如何利用 自注意力机制 (Self-Attention) 来并行处理数据,从而比传统的 RNN/LSTM 模型训练速度更快,且能更好地捕捉长距离依赖关系。

《从零开始构建智能体》第三章 大语言模型基础-读书笔记

语言模型 (Language Model, LM) 是自然语言处理的核心,其根本任务是计算一个词序列(即一个句子)出现的概率。一个好的语言模型能够告诉我们什么样的句子是通顺的、自然的。

在多智能体系统中,语言模型是智能体理解人类指令、生成回应的基础。

📝 LLM发展阶梯

LLM发展的四个层级,每一层都代表了语言模型发展的一次重要飞跃:

  • 第一级:N-gram 模型

    • 能做什么:建立了统计语言模型的基本范式,计算简单快速,结果可解释。
    • 核心思想:马尔可夫假设。
    • 局限:不懂语义,泛化能力差,且受限于固定窗口。
  • 第二级:前馈神经网络语言模型

    • 能做什么:通过词嵌入,让模型能理解词之间的语义相似性,大大增强了泛化能力。
    • 核心思想:将词映射为连续向量。
    • 局限:上下文窗口仍然是固定的,无法处理更长的历史信息。
  • 第三级:循环神经网络 (RNN/LSTM)

    • 能做什么:通过循环结构,理论上能处理任意长度的序列,拥有了“记忆”。LSTM 进一步缓解了长距离依赖问题。
    • 核心思想:信息在序列中循环传递。
    • 局限:循环计算是串行的,无法并行,训练效率低,且长距离依赖问题依然存在挑战。
  • 第四级:Transformer 与大模型

    • 能做什么:完全基于自注意力机制,实现了高效并行计算,并能直接捕捉序列中任意长距离的依赖关系,成为现代大语言模型的基础。
    • 核心思想:自注意力。
    • 局限:目前主要挑战在于计算成本高昂、推理时的显存占用等。

神经网络语言模型虽然引入了词嵌入解决了泛化问题,但它和 N-gram 模型一样,上下文窗口是固定大小的。为了预测下一个词,它只能看到前 n−1 个词,再早的历史信息就被丢弃了。这显然不符合我们人类理解语言的方式。为了打破固定窗口的限制,循环神经网络 (Recurrent Neural Network, RNN) 应运而生,其核心思想非常直观:为网络增加“记忆”能力。

RNN 的设计引入了一个隐藏状态 (hidden state) 向量,我们可以将其理解为网络的短期记忆。在处理序列的每一步,网络都会读取当前的输入词,并结合它上一刻的记忆(即上一个时间步的隐藏状态),然后生成一个新的记忆(即当前时间步的隐藏状态)传递给下一刻。这个循环往复的过程,使得信息可以在序列中不断向后传递

🧠 RNN 的核心:一个携带“记忆”的处理器

可以把RNN想象成一个有“短期记忆”的处理器。它处理信息的方式不是孤立的,而是会把对之前信息的“记忆”带到下一步的计算中。

它的工作流程就像一个“接力”过程:

  1. 读取当前输入:在每一步 t,它都会读取当前的输入信息 x_t(例如,句子中的第 t 个词)。
  2. 结合历史记忆:它会调出上一步结束时留下的“记忆”,也就是上一个隐藏状态 h_{t-1}
  3. 更新记忆:然后,它通过一个函数(可以理解为“思考”过程),把“当前输入”和“历史记忆”整合起来,形成新的记忆 h_t
  4. 传递与输出:这个新的记忆 h_t 有两个用途:一是作为这一步的“思考结果”输出(如果需要的话),二是被传递到下一步,作为下一步的“历史记忆”。

这个过程用公式简化表示就是:h_t = f( x_t, h_{t-1} )。这个循环机制,正是RNN能处理任意长度序列的关键。

📝 生动例子:理解带“上文”的句子

为了让你更直观地感受RNN的作用,我们用一个具体的句子来模拟它的处理过程。假设我们要让RNN处理下面这句话:

我 喜欢 吃 苹果

  • 处理 :RNN读取“我”。此时它没有历史记忆(或记忆为空),结合“我”和空记忆,形成新的记忆 h1,这个 h1 就编码了“我”这个信息。
  • 处理 喜欢:RNN读取“喜欢”,并调出上一步的 h1(即“我”的信息)。它把“喜欢”和“我”结合起来思考,更新记忆为 h2。此时的 h2 就包含了“我”和“喜欢”的上下文信息。
  • 处理 :RNN读取“吃”,并调出上一步的 h2(即“我 喜欢”的上下文)。它把“吃”和“我 喜欢”结合,更新记忆为 h3。此时 h3 包含了“我 喜欢 吃”的信息。
  • 处理 苹果:RNN读取“苹果”,并调出 h3(即“我 喜欢 吃”的上下文)。它结合后形成 h4,最终 h4 就完整地编码了整个句子“我 喜欢吃 苹果”的语义信息。

你看,通过这种“边读边记”的方式,RNN在处理最后一个词“苹果”时,依然清楚地记得句子开头的“我”,理解了是“我”在“吃”。这正是RNN能处理序列、捕捉上下文的关键。

🔍 与之前模型的对比

把RNN和前两代模型放在一起看,它的优势就更清楚了:

模型 核心机制 对“我 喜欢 吃 苹果”的处理方式 关键局限
N-gram 固定窗口,只看前 N-1 个词 预测“吃”后面的词时,只能看到“喜欢 吃”或“吃”,看不到开头的“我” 无法捕捉长距离依赖
前馈神经网络语言模型 词嵌入 + 固定窗口 比N-gram理解语义,但窗口依然固定,看“吃”时仍然可能漏掉“我”。 无法处理不定长序列
RNN / LSTM 循环连接 + 隐藏状态 通过隐藏状态 h把“我”的信息一步步传到“吃”和“苹果”,实现了对全句的“记忆”。 串行计算,效率较低;简单RNN有梯度问题

⚠️ RNN 的局限性

虽然RNN很巧妙,但它也有明显的短板,这直接催生了Transformer:

  • 串行计算:必须一个一个词地处理,第 t 步没算完,就不能开始 t+1 步。这导致训练无法充分利用GPU进行大规模并行计算,速度很慢。
  • 长期依赖问题:简单RNN在实践中很难记住非常久远的信息(比如几十个词之前的主语),因为信息在循环传递中会“衰减”。

💎 总结

RNN通过引入隐藏状态循环连接,实现了对序列信息的“记忆”和传递,成功突破了前代模型固定窗口的限制,能够处理任意长度的序列。这个设计在自然语言处理领域是里程碑式的。

尽管它因为串行计算和长期依赖问题,在后来被Transformer架构取代了主流地位,但它所蕴含的“让模型拥有记忆”的核心思想,至今仍在深刻影响着现代大语言模型的设计。

孤辰、寡宿也叫做隔角煞,又叫绝情煞,男怕孤辰,女怕寡宿。

在年月柱,与亲戚家人缘份薄弱,独立自主命。

在日柱,夫妻男女缘份淡薄。在时柱,与子孙缘份淡薄。

八字里带孤辰寡宿的人,人缘不是太好,缺少人际交往,兄弟朋友之间的情意也大多薄弱不堪,这也是孤寡入命终须寂的道理。

所以命中有孤辰寡宿的人都不需要太追求社交以及过分接近喧嚣场所为好,往往不得意,养身独处反而更能安然。

基本概念

孤辰与寡宿是一对关联神煞,传统多主孤独、寡居、婚姻不顺等信息。

  • 孤辰偏主”孤立、无伴”,对男性影响更常被提及
  • 寡宿偏主”寡居、失偶”,对女性影响更常被提及
  • 命中同时见孤辰与寡宿时,传统认为孤独感更重

现代解读不宜机械断孤寡,应结合十神组合、婚姻宫状态、桃花等综合判断。

取法口诀

寅卯辰人孤巳丑,巳午未人孤申辰,申酉戌人孤亥未,亥子丑人孤寅戌

口诀解读:

  • “孤X”指孤辰在X,”丑/辰/未/戌”指寡宿

地支对照表

季节分组(非三合局),以年支或日支为基准:

地支分组(季节) 孤辰 寡宿
寅卯辰(春)
巳午未(夏)
申酉戌(秋)
亥子丑(冬)

推导规律:

  • 孤辰 = 下一季节的第一个地支
  • 寡宿 = 上一季节的最后一个地支

记忆

方法一

地支三会划分

男怕孤辰,女怕寡宿
从十二长生位讲,孤辰是病地,寡宿是冠带

男人就是最讨厌-无病呻吟
女人就是最讨厌-情关(冠带之年)

方法二

孤辰、寡宿也叫做隔角煞

正是三会隔角的位置:

寅申巳亥四长生(病地) - 孤辰
辰戌丑未四墓地 (冠带)- 寡宿