Commit on 2025/03/24 周一 14:44:11.37

This commit is contained in:
zhangsan 2025-03-24 14:44:11 +08:00
parent 7f37484565
commit 46b79a40cb
6 changed files with 367 additions and 327 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output/

View File

@ -7,7 +7,7 @@
**数组集合比较**
#### **数组集合比较**
**`Arrays.equals(array1, array2)`**
@ -15,17 +15,16 @@
- 支持多种类型的数组(如 `int[]``char[]``Object[]` 等)。
- ```
- ```text
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
boolean isEqual = Arrays.equals(arr1, arr2); // true
```text
`Collections` 类本身没有直接提供类似 `Arrays.equals` 的方法来比较两个集合的内容是否相等。不过Java 中的集合类(如 `List``Set``Map`)已经实现了 `equals` 方法
- ```
- ```text
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(1, 2, 3);
List<Integer> list3 = Arrays.asList(3, 2, 1);
@ -35,76 +34,6 @@
要实现接口自定义排序,必须实现 `Comparator<T>` 接口的 `compare(T o1, T o2)` 方法。
`Comparator` 接口中定义的 `compare(T o1, T o2)` 方法返回**一个整数**(非布尔值!!),这个整数的正负意义如下:
- 如果返回负数,说明 `o1` 排在 `o2`前面。
- 如果返回零,说明 `o1` 等于 `o2`
- 如果返回正数,说明 `o1` 排在 `o2`后面。
```text
public class TestComparator {
// 定义一个升序排序的 Comparator
static Comparator<Integer> ascComparator = new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b; // 如果 a < b, 则返回负数
}
};
public static void main(String[] args) {
// 创建一个整数列表
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(3);
numbers.add(8);
numbers.add(1);
numbers.add(9);
numbers.add(2);
// 使用 Collections.sort 进行排序,并指定 Comparator
Collections.sort(numbers, ascComparator);
// 输出排序后的列表
System.out.println("排序后的列表: " + numbers);
}
}
```
假设有两个参数a=3,b=5那么返回负数表示第一个参数a排在第二个参数b前面因此是升序
**自定义比较器排序二维数组** 用Lambda表达式实现`Comparator<int[]>接口`
```text
import java.util.Arrays;
public class IntervalSort {
public static void main(String[] args) {
int[][] intervals = { {1, 3}, {2, 6}, {8, 10}, {15, 18} };
// 自定义比较器,先比较第一个元素,如果相等再比较第二个元素
Arrays.sort(intervals, (a, b) -> {
if (a[0] != b[0]) {
return a[0] - b[0];
} else {
return a[1] - b[1];
}
});
// 输出排序结果
for (int[] interval : intervals) {
System.out.println(Arrays.toString(interval));
}
}
}
```
**逻辑比较**
```text
@ -148,8 +77,6 @@ String sortedStr = new String(charArray);
取字符:
- `charAt(int index)` 方法返回指定索引处的 `char` 值。
@ -169,7 +96,7 @@ String sortedStr = new String(charArray);
- 不保证元素的顺序。
- ```
- ```text
import java.util.HashMap;
import java.util.Map;
@ -205,7 +132,7 @@ String sortedStr = new String(charArray);
System.out.println("After removal: " + map); // 输出 {apple=10, banana=20}
}
}
```text
```
@ -217,7 +144,7 @@ String sortedStr = new String(charArray);
- 在指定位置插入和删除O(n) `add(int index, E element)` `remove(int index)`
- ```
- ```text
import java.util.ArrayList;
import java.util.List;
@ -258,6 +185,33 @@ String sortedStr = new String(charArray);
}
```
**如果事先不知道嵌套列表的大小如何遍历呢?**
```text
import java.util.ArrayList;
import java.util.List;
int rows = 3;
int cols = 3;
List<List<Integer>> list = new ArrayList<>();
for (List<Integer> row : list) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println(); // 换行
}
for (int i = 0; i < list.size(); i++) {
List<Integer> row = list.get(i);
for (int j = 0; j < row.size(); j++) {
System.out.print(row.get(j) + " ");
}
System.out.println(); // 换行
}
```
#### **`数组Array`**
@ -270,7 +224,7 @@ String sortedStr = new String(charArray);
- **连续内存**:数组的元素在内存中是连续存储的。
- ```
- ```text
public class ArrayExample {
public static void main(String[] args) {
// 创建数组
@ -334,13 +288,12 @@ String sortedStr = new String(charArray);
System.out.println("Array length: " + length); // 输出 5
}
}
```text
#### **二维数组**
```
```text
int rows = 3;
int cols = 3;
int[][] array = new int[rows][cols];
@ -372,48 +325,7 @@ public void setZeroes(int[][] matrix) {
System.out.println(); // 换行,便于输出格式化
}
}
```text
```
import java.util.ArrayList;
import java.util.List;
int rows = 3;
int cols = 3;
List<List<Integer>> list = new ArrayList<>();
for (List<Integer> row : list) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println(); // 换行
}
for (int i = 0; i < list.size(); i++) {
List<Integer> row = list.get(i);
for (int j = 0; j < row.size(); j++) {
System.out.print(row.get(j) + " ");
}
System.out.println(); // 换行
}
```text
**如果事先不知道数组的大小呢?**
```
List<int[]> merged = new ArrayList<>();
merged.add(current);
return merged.toArray(new int[merged.size()][]);
```text
#### **`HashSet`**
@ -422,7 +334,7 @@ return merged.toArray(new int[merged.size()][]);
- 不保证元素的顺序!!因此不太用iterator迭代而是用contains判断是否有xx元素。
- ```
```text
import java.util.HashSet;
import java.util.Set;
@ -455,11 +367,7 @@ return merged.toArray(new int[merged.size()][]);
System.out.println("After removal: " + set); // 输出 [10, 30]
}
}
```
```
@ -495,7 +403,7 @@ return merged.toArray(new int[merged.size()][]);
6. **`clear()`**
- 清空队列。
- ```
- ```text
import java.util.PriorityQueue;
import java.util.Comparator;
@ -545,16 +453,15 @@ return merged.toArray(new int[merged.size()][]);
System.out.println("队列是否为空: " + minHeap.isEmpty()); // 输出 true
}
}
```text
#### `Queue`
队尾插入,队头取!``
队尾插入,队头取!
```
```text
import java.util.Queue;
import java.util.LinkedList;
@ -584,9 +491,7 @@ public class QueueExample {
System.out.println("队列内容:" + queue);
}
}
```text
```
#### `Deque`(双端队列+栈)
@ -595,22 +500,20 @@ public class QueueExample {
建议在需要栈操作时使用 `Deque` 的实现
**栈**
```
```text
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // 入栈
Integer top1=stack.peek()
Integer top = stack.pop(); // 出栈
```text
```
**双端队列**
**在队头操作**
*在队头操作*
- `addFirst(E e)`:在队头添加元素,如果操作失败会抛出异常。
- `offerFirst(E e)`:在队头插入元素,返回 `true``false` 表示是否成功。
@ -618,7 +521,7 @@ Integer top = stack.pop(); // 出栈
- `removeFirst()`:移除并返回队头元素;队列为空会抛出异常。
- `pollFirst()`:移除并返回队头元素;队列为空返回 `null`
**在队尾操作**
*在队尾操作*
- `addLast(E e)`:在队尾添加元素,若失败会抛出异常。
- `offerLast(E e)`:在队尾插入元素,返回 `true``false` 表示是否成功。
@ -628,13 +531,11 @@ Integer top = stack.pop(); // 出栈
**添加元素**:调用 `add(e)``offer(e)` 时,实际上是调用 `addLast(e)``offerLast(e)`,即在**队尾**添加元素。
*添加元素*:调用 `add(e)``offer(e)` 时,实际上是调用 `addLast(e)``offerLast(e)`,即在**队尾**添加元素。
**删除或查看元素**:调用 `remove()``poll()` 时,则是调用 `removeFirst()``pollFirst()`,即在队头移除元素;同理,`element()``peek()` 则是查看队头元素。
*删除或查看元素*:调用 `remove()``poll()` 时,则是调用 `removeFirst()``pollFirst()`,即在队头移除元素;同理,`element()``peek()` 则是查看队头元素。
```
```text
import java.util.Deque;
import java.util.LinkedList;
@ -670,8 +571,7 @@ public class DequeExample {
System.out.println("双端队列最终内容:" + deque);
}
}
```text
```
@ -685,7 +585,7 @@ public class DequeExample {
2. `next()`:返回迭代器的下一个元素,并将迭代器移动到下一个位置。
3. `remove()`:从迭代器当前位置删除元素。该方法是可选的,不是所有的迭代器都支持。
```
```text
import java.util.ArrayList;
import java.util.Iterator;
@ -707,7 +607,7 @@ public class Main {
}
}
}
```text
```
@ -719,7 +619,7 @@ public class Main {
#### **数组排序**
```
```text
import java.util.Arrays;
public class ArraySortExample {
@ -729,17 +629,20 @@ public class ArraySortExample {
System.out.println(Arrays.toString(numbers)); // 输出 [1, 2, 5, 5, 6, 9]
}
}
```text
```
#### 集合排序
```
```text
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ListSortExample {
public static void main(String[] args) {
// 创建一个 ArrayList 并添加元素
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
numbers.add(2);
@ -748,15 +651,28 @@ public class ListSortExample {
numbers.add(5);
numbers.add(6);
Collections.sort(numbers); // 对 List 进行排序
// 对 List 进行排序
Collections.sort(numbers);
// 输出排序后的 List
System.out.println(numbers); // 输出 [1, 2, 5, 5, 6, 9]
}
}
```text
```
#### **自定义排序**
```
要实现接口自定义排序,必须实现 `Comparator<T>` 接口的 `compare(T o1, T o2)` 方法。
`Comparator` 接口中定义的 `compare(T o1, T o2)` 方法返回**一个整数**(非布尔值!!),这个整数的正负意义如下:
- 如果返回负数,说明 `o1` 排在 `o2`前面。
- 如果返回零,说明 `o1` 等于 `o2`
- 如果返回正数,说明 `o1` 排在 `o2`后面。
```text
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -779,6 +695,7 @@ class Person {
public class ComparatorSortExample {
public static void main(String[] args) {
// 创建一个 Person 列表
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25));
people.add(new Person("Bob", 20));
@ -792,10 +709,39 @@ public class ComparatorSortExample {
}
});
// 输出排序后的列表
System.out.println(people); // 输出 [Alice (25), Bob (20), Charlie (30)]
}
}
```
**自定义比较器排序二维数组** 用Lambda表达式实现`Comparator<int[]>接口`
```text
import java.util.Arrays;
public class IntervalSort {
public static void main(String[] args) {
int[][] intervals = { {1, 3}, {2, 6}, {8, 10}, {15, 18} };
// 自定义比较器,先比较第一个元素,如果相等再比较第二个元素
Arrays.sort(intervals, (a, b) -> {
if (a[0] != b[0]) {
return a[0] - b[0];
} else {
return a[1] - b[1];
}
});
// 输出排序结果
for (int[] interval : intervals) {
System.out.println(Arrays.toString(interval));
}
}
}
```
@ -854,7 +800,7 @@ public class ComparatorSortExample {
### 前缀和
#### 前缀和
1. **前缀和的定义**
定义前缀和 `preSum[i]` 为数组 `nums` 从索引 0 到 i 的元素和,即

View File

@ -18,7 +18,7 @@ git clone地址http://47.98.59.178:3000/zy123/zbparse.git
## 项目启动与维护:
![1](https://pic.bitday.top/i/2025/03/19/u7gwuk-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inur4e-2.png)
.env存放一些密钥大模型、textin等它是gitignore忽略了因此在服务器上git pull项目的时候这个文件不会更新因为密钥比较重要需要手动维护服务器相应位置的.env。
@ -28,7 +28,7 @@ git clone地址http://47.98.59.178:3000/zy123/zbparse.git
1. 进入项目文件夹
![1](https://pic.bitday.top/i/2025/03/19/u7rz9c-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inryfa-2.png)
**注意:**需要确认.env是否存在在服务器默认是隐藏的
输入cat .env
@ -54,7 +54,7 @@ requirements.txt一般无需变动除非代码中使用了新的库也要
**docker-compose:**
![1](https://pic.bitday.top/i/2025/03/19/u7r7e6-2.png)
![1](https://pic.bitday.top/i/2025/03/24/io7gar-2.png)
本项目为**单服务项目**只有flask_app(服务名)
@ -67,7 +67,7 @@ build context`context: .`
**dockerfile:**
![1](https://pic.bitday.top/i/2025/03/19/u7uvxt-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inv2qr-2.png)
COPY . .(在 Dockerfile 中):
这条指令会将构建上下文中的所有内容复制到镜像中的当前工作目录(这里是 `/flask_project`)。
@ -111,9 +111,9 @@ bin boot dev etc flask_project home lib lib64 media mnt opt proc roo
**序号**:如果同一个服务启动了多个容器,会有数字序号来区分(这里是 `1`)。
docker-compose exec **flask_app** sh
docker-compose exec flask_app sh
docker exec -it **zbparse-flask_app-1** sh
docker exec -it zbparse-flask_app-1 sh
这两个是等价的因为docker-compose 会自动找到对应的完整容器名并执行命令。
@ -138,9 +138,9 @@ docker image prune
2. .env环境配好 一般不需要在电脑环境变量中额外配置了但是要在Pycharm中**安装插件**,使得项目在**启动时**能将env中的环境变量**自动配置**到系统环境变量中!!!)
3. 点击下拉框Edit configurations
![1](https://pic.bitday.top/i/2025/03/19/u7trp1-2.png)
![1](https://pic.bitday.top/i/2025/03/24/ins4wd-2.png)
设置run_serve.py为启动脚本![1](https://pic.bitday.top/i/2025/03/19/u7viek-2.png)
设置run_serve.py为启动脚本![1](https://pic.bitday.top/i/2025/03/24/io729q-2.png)
注意这里的working directory要设置到最外层文件夹而不是flask_app
@ -185,35 +185,35 @@ python flask_app\run_serve.py
- 打开 Anaconda Prompt然后输入 `where conda` 来查看 conda 的路径。
- ```
- ```text
打开系统环境变量Path添加一条C:\ProgramData\anaconda3\condabin
或者 CMD 中 set PATH=%PATH%;新添加的路径
```text
```
- 重启终端可以刷新环境变量
3.如果你尚未在 PowerShell 中初始化 conda可以在 Anaconda Prompt 中运行:
```
conda init powershell
```text
conda init powershell
```
4.进入到存放run.ps1文件的目录在搜索栏中输入powershell
5.默认情况下PowerShell 可能会阻止运行脚本。你可以调整执行策略:
```
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```text
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
6.运行脚本
```
.\run.ps1
```text
.\run.ps1
```
**注意!!!**
@ -256,7 +256,7 @@ bid-assistance/test 里面找个文件的url推荐'094定稿-湖北工业大
清理/home/Z/zbparse_output_dev下的output1这些二级目录下的c8d2140d-9e9a-4a49-9a30-b53ba565db56这种uuid的三级目录只保留最近7天
```
```text
#!/bin/bash
# 需要清理的 output 目录路径
@ -277,28 +277,28 @@ find "$ROOT_DIR" -mindepth 2 -depth -type d -mtime +7 -print -exec rm -rf {} \;
echo "清理完成。"
```text
```
2. 添加权限。
```
sudo chmod +x ./clean_dir.sh
```text
sudo chmod +x ./clean_dir.sh
```
3. 执行
```
sudo ./clean_dir.sh
```text
sudo ./clean_dir.sh
```
4. 以 root 用户的身份编辑 crontab 文件从而设置或修改系统定时任务cron jobs。每天零点10分清理
```
```text
sudo crontab -e
在里面添加:
10 0 * * * /home/Z/clean_dir.sh
```text
```
**目前测试服务器和正式服务器都写上了!无需变动**
@ -310,7 +310,7 @@ sudo crontab -e
**查看容器运行时占用的文件FD套接字FD等**(排查内存泄漏,长期运行这三个值不会很大)
```
```text
[Z@iZbp13rxxvm0y7yz7l02hbZ zbparse]$ docker exec -it zbparse-flask_app-1 sh
ls -l /proc/1/fd | awk '
@ -329,13 +329,13 @@ END {
print "管道FD:", pipe
print "其他FD:", other
}'
```text
```
**可以发现文件FD很大基本上发送一个请求文件FD就加一且不会衰减**
经排查,@validate_and_setup_logger注解会为每次请求都创建一个logger,需要在@app.teardown_request中获取与本次请求有关的logger并释放。
```
```text
def create_logger(app, subfolder):
"""
创建一个唯一的 logger 和对应的输出文件夹。
@ -362,7 +362,7 @@ def create_logger(app, subfolder):
logger.propagate = False
g.logger = logger
g.output_folder = output_folder #输出文件夹路径
```text
```
handler每当 logger 生成一条日志信息时,这条信息会被传递给所有关联的 handler由 handler 决定如何输出这条日志。例如,`FileHandler` 会把日志写入文件,而 `StreamHandler` 会将日志输出到控制台。
@ -386,7 +386,7 @@ handler每当 logger 生成一条日志信息时,这条信息会被传递
pip install **memory_profiler**
```
```text
from memory_profiler import memory_usage
import time
@profile
@ -402,18 +402,18 @@ mem_before = memory_usage()[0]
result=my_function()
mem_after = memory_usage()[0]
print(f"Memory before: {mem_before} MiB, Memory after: {mem_after} MiB")
```text
```
@profile注解加在函数上,可以逐行分析内存增减情况。
memory_usage()[0] 可以获取当前程序所占内存的**快照**
![1](https://pic.bitday.top/i/2025/03/19/u7vqf9-2.png)
![1](https://pic.bitday.top/i/2025/03/24/intiph-2.png)
产生的数据都存到result变量-》内存中这是正常的因此my_function没有内存泄漏问题。
**但是**
```
```text
@profile
def extract_text_by_page(file_path):
result = ""
@ -425,9 +425,9 @@ def extract_text_by_page(file_path):
page = reader.pages[page_num]
text = page.extract_text()
return ""
```text
```
![1](https://pic.bitday.top/i/2025/03/19/u7ukfk-2.png)
![1](https://pic.bitday.top/i/2025/03/24/invnrj-2.png)
可以发现尽管我返回""内存仍然没有释放因为就是读取pdf这块发生了内存泄漏
@ -435,7 +435,7 @@ def extract_text_by_page(file_path):
**tracemalloc**
```
```text
def extract_text_by_page(file_path):
result = ""
with open(file_path, 'rb') as file:
@ -463,9 +463,9 @@ for stat in stats[:10]:
print(stat)
# 停止内存分配跟踪
tracemalloc.stop()
```text
```
![1](https://pic.bitday.top/i/2025/03/19/u7vdu0-2.png)
![1](https://pic.bitday.top/i/2025/03/24/io6evy-2.png)
tracemalloc能更深入的分析不仅是自己写的代码**调用的库函数**产生的内存也能分析出来。在这个例子中就是PyPDF2中的各个函数占用了大部分内存。
@ -477,9 +477,9 @@ tracemalloc能更深入的分析不仅是自己写的代码**调用的库
1. 首先尝试用with open打开文件代替直接使用
```
reader =PdfReader(file_path)
```text
reader =PdfReader(file_path)
```
能够确保文件正常关闭。但是没有效果。
@ -499,7 +499,7 @@ reader =PdfReader(file_path)
**main_func**是真正执行的函数!!!
```
```text
def _child_target(main_func, queue, output_folder, file_path, file_type, unique_id):
"""
子进程中调用 `main_func`(它是一个生成器函数),
@ -532,7 +532,7 @@ def run_in_subprocess(main_func, output_folder, file_path, file_type, unique_id)
yield item
p.join()
```text
```
如果开子线程,线程共享同一进程的内存空间,所以如果发生内存泄漏,泄漏的内存会累积在整个进程中,影响所有线程。
@ -546,7 +546,7 @@ def run_in_subprocess(main_func, output_folder, file_path, file_type, unique_id)
如果是Waitress服务器启动这里的进程池是全局共享的但如果Gunicorn启动每个请求分配一个worker进程进程池是在worker里面共享的
```
```text
#创建app,启动时
def create_app():
# 创建全局日志记录器
@ -562,11 +562,11 @@ def judge_zbfile_exec_sub(file_path):
args=(file_path,)
)
return result
```text
```
但是存在一个问题:**第一次发送请求执行时间较慢!**
![1](https://pic.bitday.top/i/2025/03/19/u7us5y-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inwuya-2.png)
可以发现实际执行只需7.7s但是接口实际耗时10.23秒,主要是因**懒加载或按需初始化**:有些模块或资源在子进程启动时并不会马上加载,而是在子进程首次真正执行任务时才进行初始化。
@ -576,7 +576,7 @@ def judge_zbfile_exec_sub(file_path):
**还可以快速验证服务是否正常启动**
```
```text
def warmup_request():
# 等待服务器完全启动,例如等待 1-2 秒
time.sleep(5)
@ -589,7 +589,7 @@ def warmup_request():
print(f"Warm-up 请求发送成功,状态码:{response.status_code}")
except Exception as e:
print(f"Warm-up 请求出错:{e}")
```text
```
threading.Thread(target=warmup_request, daemon=True).start()
@ -597,7 +597,7 @@ threading.Thread(target=warmup_request, daemon=True).start()
## flask_app结构介绍
<img src="https://pic.bitday.top/i/2025/03/19/u7i429-2.png" alt="1" style="zoom:67%;" />
<img src="https://pic.bitday.top/i/2025/03/24/io7n4q-2.png" alt="1" style="zoom:67%;" />
@ -609,7 +609,7 @@ threading.Thread(target=warmup_request, daemon=True).start()
账号分流qianwen_plus下的
```
```text
api_keys = cycle([
os.getenv("DASHSCOPE_API_KEY"),
# os.getenv("DASHSCOPE_API_KEY_BACKUP1"),
@ -621,7 +621,7 @@ def get_next_api_key():
return next(api_keys)
api_key = get_next_api_key()
```text
```
只需轮流使用不同的api_key即可。目前没有启用。
@ -634,31 +634,31 @@ general/llm下的doubao.py 和通义千问long_plus.py
1. 这是qianwen-long的限制针对阿里qpm为1200每秒就是20又linux和windows服务器对半就是10TPM无上限
```
```text
@sleep_and_retry
@limits(calls=10, period=1) # 每秒最多调用10次
def rate_limiter():
pass # 这个函数本身不执行任何操作,只用于限流
```text
```
2. 这是qianwen-plus的限制针对tpm为1000万每个请求2万tokens那么linux和windows总的qps为8时8x60x2=960<1000单个为4
**经过2.11号测试calls=4时最高TPM为800因此把目前稳定版把calls设为5**
**2.12,用turbo作为超限后的承载目前把calls设为7**
```
```text
@sleep_and_retry
@limits(calls=7, period=1) # 每秒最多调用7次
def qianwen_plus(user_query, need_extra=False):
logger = logging.getLogger('model_log') # 通过日志名字获取记录器
```text
```
3. qianwen_turbo的限制TPM为500万由于它是plus后的手段稳妥一点qps设为6两个服务器分流即calls=3
```
```text
@sleep_and_retry
@limits(calls=3, period=1) # 500万tpm每秒最多调用6次两个服务器分流就是3次 plus超限后的保底手段稳妥一点
```text
```
**重点!!**后续阿里扩容之后成倍修改这块**calls=?**
@ -670,20 +670,20 @@ def qianwen_plus(user_query, need_extra=False):
1. start_up.py的def create_app()函数限制了对每个接口同时100次请求。这里事实上不再限制了因为100已经足够大了默认限制做到大模型限制这块。
```
```text
app.connection_limiters['upload'] = ConnectionLimiter(max_connections=100)
app.connection_limiters['get_deviation'] = ConnectionLimiter(max_connections=100)
app.connection_limiters['default'] = ConnectionLimiter(max_connections=100)
app.connection_limiters['judge_zbfile'] = ConnectionLimiter(max_connections=100)
```text
```
2. ConnectionLimiter.py以及每个接口上的装饰器
```
```text
@require_connection_limit(timeout=1800)
def zbparse():
```text
```
这里限制了每个接口内部执行的时间暂时设置到了30分钟不包括排队时间超时就是解析失败
@ -697,7 +697,7 @@ app.connection_limiters['upload'] = ConnectionLimiter(max_connections=100)
是公共函数存放的文件夹llm下是各类大模型读取文件下是docx pdf文件的读取以及文档清理clean_pdf去页眉页脚页码
![1](https://pic.bitday.top/i/2025/03/19/u7sxx7-2.png)
![1](https://pic.bitday.top/i/2025/03/24/insu41-2.png)
general下的llm下的清除file_id.py 需要**每周运行至少一次**防止file_id数量超出我这边对每次请求结束都有file_id记录并清理向应该还没加
@ -725,7 +725,7 @@ post_processing中的**process_functions_in_parallel**提取
资格审查、技术偏离、 商务偏离、 所需提交的证明材料
![1](https://pic.bitday.top/i/2025/03/19/u7sqz1-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inx4nv-2.png)
大解析upload用了post_processing完整版
@ -755,9 +755,9 @@ get_deviation.py、偏离表数据解析main.py用了process_functions_in_parall
若开头没截准就改begin_pattern末尾没截准就改end_pattern
![1](https://pic.bitday.top/i/2025/03/19/u7r56h-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inxido-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7hd6v-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inrfjf-2.png)
另外:在*截取pdf货物标版*.py中还有extract_pages_twice函数即第一次没有切分到之后会运行该函数这边又有一套begin_pattern和end_pattern即二次提取
@ -765,7 +765,7 @@ get_deviation.py、偏离表数据解析main.py用了process_functions_in_parall
**如何测试?**
![1](https://pic.bitday.top/i/2025/03/19/u7vy07-2.png)
![1](https://pic.bitday.top/i/2025/03/24/insag6-2.png)
输入pdf_path和你要切分的序号selection=1代表切公告依次类推可以看切出来的效果如何。
@ -778,7 +778,7 @@ get_deviation.py、偏离表数据解析main.py用了process_functions_in_parall
这里如果段落中既被正则匹配又被follow_up_keywords中的任意一个匹配那么不会添加到temp中即不会被大模型筛选它会**直接添加**到最后的返回中!
![1](https://pic.bitday.top/i/2025/03/19/u7hwyg-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inwco3-2.png)
@ -796,7 +796,7 @@ get_deviation.py、偏离表数据解析main.py用了process_functions_in_parall
都是废弃文件代码,未在正式、测试环境中使用的,不用管
![1](https://pic.bitday.top/i/2025/03/19/u7rphb-2.png)
![1](https://pic.bitday.top/i/2025/03/24/invb01-2.png)
@ -804,7 +804,7 @@ get_deviation.py、偏离表数据解析main.py用了process_functions_in_parall
是接口以及主要实现部分,一一对应
![1](https://pic.bitday.top/i/2025/03/19/u7v3r9-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inx972-2.png)
get_deviation对应偏离表数据解析main获得偏离表数据
@ -824,7 +824,7 @@ upload对应工程标解析和货物标解析即大解析
utils是接口这块的公共功能函数。其中validate_and_setup_logger函数对不同的接口请求对应到不同的output文件夹如upload->output1。后续增加接口也可直接在这里写映射关系。
![1](https://pic.bitday.top/i/2025/03/19/u7tjt1-2.png)
![1](https://pic.bitday.top/i/2025/03/24/intx0g-2.png)
重点关注大解析:**upload.py**和**货物标解析main.py**
@ -838,7 +838,7 @@ utils是接口这块的公共功能函数。其中validate_and_setup_logger函
各个文件夹(output1 output2..)对应不同的接口请求
![1](https://pic.bitday.top/i/2025/03/19/u7h4vo-2.png)
![1](https://pic.bitday.top/i/2025/03/24/int1ko-2.png)
@ -850,7 +850,7 @@ testdir是平时写代码的测试的地方
它们都不影响正式和测试环境的解析
![1](https://pic.bitday.top/i/2025/03/19/u7hpp5-2.png)
![1](https://pic.bitday.top/i/2025/03/24/insm8e-2.png)
@ -858,7 +858,7 @@ testdir是平时写代码的测试的地方
是两个解析流程中不一样的地方(一样的都写在**general**中了)
![1](https://pic.bitday.top/i/2025/03/19/u7x9km-2.png)
![1](https://pic.bitday.top/i/2025/03/24/io6pba-2.png)
主要是货物标额外解析了采购要求提取采购需求main+技术参数要求提取+商务服务其他要求提取)
@ -868,7 +868,7 @@ testdir是平时写代码的测试的地方
ConnectionLimiter.py定义了接口超时时间->超时后断开与后端的连接
![1](https://pic.bitday.top/i/2025/03/19/u7w6yz-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inxrbo-2.png)
logger_setup.py 为每个请求创建单独的log每个log对应一个log.txt
@ -878,14 +878,14 @@ start_up.py是启动脚本run_serve也是启动脚本是对start_up.py的
## 持续关注
```
```text
yield sse_format(tech_deviation_response)
yield sse_format(tech_deviation_star_response)
yield sse_format(zigefuhe_deviation_response)
yield sse_format(shangwu_deviation_response)
yield sse_format(shangwu_star_deviation_response)
yield sse_format(proof_materials_response)
```text
```
1. 工程标解析目前仍没有解析采购要求这一块,因此后处理返回的只有'资格审查'和''证明材料"和"extracted_info",没有''商务偏离''及'商务带星偏离',也没有'技术偏离'和'技术带星偏离',而货物标解析是完全版。
@ -894,13 +894,13 @@ start_up.py是启动脚本run_serve也是启动脚本是对start_up.py的
2. 大解析中返回了技术评分,后端接收后不仅显示给前端,还会返给向,用于生成技术偏离表
3. 小解析时get_deviation.py其实也可以返回技术评分但是没有返回因为没人和我对接暂时注释了。
![1](https://pic.bitday.top/i/2025/03/19/u7t9d7-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inu7kk-2.png)
4.商务评议和技术评议偏离表,即评分细则的偏离表,暂时没做,但是**商务评分、技术评分**无论大解析还是小解析都解析了,稍微对该数据处理一下返回给后端就行。
![1](https://pic.bitday.top/i/2025/03/19/u7rf1o-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inu40r-2.png)
这个是解析得来的结果,适合给前端展示,但是要生成商务技术评议偏离表的话,需要再调一次大模型,对该数据进行重新归纳,以字符串列表为佳。再传给后端。(未做)
@ -930,7 +930,7 @@ start_up.py是启动脚本run_serve也是启动脚本是对start_up.py的
Flask 和 Waitress 是两个不同层级的工具,在 Python Web 开发中扮演互补角色。它们的协作关系可以概括为:**Flask 负责构建 Web 应用逻辑,而 Waitress 作为生产级服务器承载 Flask 应用**。
```
```text
# Flask 开发服务器(仅用于开发)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
@ -938,7 +938,7 @@ if __name__ == '__main__':
# 使用 Waitress 启动(生产环境)
from waitress import serve
serve(app, host='0.0.0.0', port=8080)
```text
```
**Waitress 的工作方式**
@ -984,16 +984,16 @@ serve(app, host='0.0.0.0', port=8080)
要使用异步 worker你需要
```
pip install gevent
```text
pip install gevent
```
启动 Gunicorn 时指定 worker 类型和数量,例如:
```
```text
gunicorn -k gevent -w 4 --max-requests 100 flask_app.start_up:create_app --bind 0.0.0.0:5000
```text
```
使用 `-k gevent`(或者 `-k eventlet`)就可以使用异步 worker单个 worker 能够处理多个 I/O 密集型请求。
@ -1061,9 +1061,9 @@ Python特别是 CPython 实现中有一个叫做全局解释器锁Globa
**multiprocessing.Pool库**,通过 `maxtasksperchild` 指定每个子进程在退出前最多执行的任务数,这有助于防止某些任务中可能存在的内存泄漏问题
```
pool =Pool(processes=10, maxtasksperchild=3)
```text
pool =Pool(processes=10, maxtasksperchild=3)
```
**concurrent.futures.ProcessPoolExecutor**更高级、更统一,没有类似 `maxtasksperchild` 的参数,意味着进程在整个执行期内会一直存活,适合任务本身**比较稳定**的场景。
@ -1081,7 +1081,7 @@ pool =ProcessPoolExecutor(max_workers=10)
- 线程之间可以直接共享全局变量、对象或数据结构,不需要额外的序列化过程,但这也带来了同步的复杂性(如竞态条件)。
```
```text
import threading
num=0
def work():
@ -1105,17 +1105,17 @@ if __name__ == '__main__':
t1.join()
t2.join()
print('主线程执行结果',num)
```text
```
运行结果:
```
```text
work 1551626
work1 1615783
主线程执行结果 1615783
```text
```
这些数值都小于预期的 2000000因为
@ -1137,7 +1137,7 @@ work1 1615783
**解决:**
```
```text
from threading import Lock
import threading
@ -1166,7 +1166,7 @@ if __name__ == '__main__':
t2.join()
print('主线程执行结果',num)
```text
```
@ -1175,7 +1175,7 @@ if __name__ == '__main__':
- 进程之间默认不共享内存,因此如果需要传递数据,就必须使用专门的通信机制。
- 在 Python 中,可以使用 `multiprocessing.Queue``multiprocessing.Pipe`、共享内存(如 `multiprocessing.Value``multiprocessing.Array`)等方式实现进程间通信。
```
```text
from multiprocessing import Process, Queue
def worker(process_id, q):
@ -1200,7 +1200,7 @@ if __name__ == '__main__':
results.append(q.get())
print("Collected data:", results)
```text
```
- 当你在主进程中创建了一个 `Queue` 对象,然后将它作为参数传递给子进程时,子进程会获得一个能够与主进程通信的“句柄”。
@ -1212,22 +1212,22 @@ if __name__ == '__main__':
### 项目贡献
![1](https://pic.bitday.top/i/2025/03/19/u7i84d-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inufxm-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7s5x0-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inwn43-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7wpu4-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inw4fh-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7tzvp-2.png)
![1](https://pic.bitday.top/i/2025/03/24/invvo2-2.png)
### 效果图
![1](https://pic.bitday.top/i/2025/03/19/u7wvd4-2.png)
![1](https://pic.bitday.top/i/2025/03/24/io7051-2.png)
![](https://pic.bitday.top/i/2025/03/19/u7u7qh-2.gif)
![](https://pic.bitday.top/i/2025/03/24/intlc7-2.gif)
![1](https://pic.bitday.top/i/2025/03/19/u7smek-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inrn7m-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7tfw3-2.png)
![1](https://pic.bitday.top/i/2025/03/24/inv0t0-2.png)
![1](https://pic.bitday.top/i/2025/03/19/u7x3g6-2.png)
![1](https://pic.bitday.top/i/2025/03/24/int4jz-2.png)

View File

@ -532,8 +532,6 @@ JdkSerializationRedisSerializer**导致我们存到Redis中后的数据和原
### 功能测试
**通过RedisTemplate对象操作Redis**

View File

@ -263,27 +263,6 @@ h_v^{(k+1)} = \sigma \Big(
$$
### 直推式学习与归纳式学习
**直推式学习Transductive Learning**
模型直接在固定的训练图上学习节点的表示或标签,结果只能应用于这张图中的节点,无法直接推广到新的、未见过的节点或图。
例如DeepWalk ,它通过对固定图的随机游走生成节点序列来学习节点嵌入,因此只能得到训练图中已有节点的表示,一旦遇到新节点,需要重新训练或进行特殊处理。
**注意**GCN是直推式的因为它依赖于整个图的归一化邻接矩阵进行卷积操作需要在固定图上训练。GraphSAGE 是归纳式学习方法。它通过在每一层随机采样固定数量的邻居,当有新节点加入时,你可以构造一个包含新节点及其局部邻居的子图,然后重新计算该局部子图的 $\tilde{A}$ 和 $\tilde{D}$ 矩阵。这样就不需要对整个图做全局归一化
**归纳式学习Inductive Learning**
模型学习的是一个映射函数或规则,可以将这种规则推广到未见过的**新节点**或**新图**上。这种方法能够处理动态变化的图结构或新的数据。
例如:图神经网络的变体都是归纳式的,因为它们在聚合邻居信息时学习一个共享的函数,该函数能够应用于任意新节点。
**泛化到新节点**在许多推荐系统中如果有新用户加入新节点我们需要给他们做个性化推荐这就要求系统能够在不重新训练整个模型的情况下为新用户生成表示Embedding并且完成推荐预测。
**泛化到新图:** 分子图预测。我们会用一批训练分子(每个分子是一张图)来训练一个 GNN 模型,让它学会如何根据图结构与原子特征来预测分子的某些性质(如毒性、溶解度、活性等)。训练完成后,让它在新的分子上做预测。
## GAT
@ -318,7 +297,7 @@ $$
{\sum_{k\in \mathcal{N}_i} \exp\Bigl(\text{LeakyReLU}\bigl(\mathbf{a}^\top \bigl[\;W\mathbf{h}_i \,\|\, W\mathbf{h}_k\bigr]\bigr)\Bigr)}
$$
- 其中,$\mathbf{a}$ 为可学习的参数向量,$\|$ 表示向量拼接concatenation
- 其中,$\mathbf{a}$ 为可学习的参数向量,$\|$ 表示向量拼接concatenation
**示例假设:**
@ -355,7 +334,6 @@ $$
W\mathbf{h}_k = \begin{bmatrix}1 \\ 1\end{bmatrix}
$$
2. **构造拼接向量并计算未归一化的注意力系数 $e_{ij}$ 和 $e_{ik}$**
- 对于邻居 $j$
@ -394,6 +372,77 @@ $$
### 特征聚合
**单头注意力聚合(得到新的节点特征)**
$$
\mathbf{h}_i' = \sigma\Bigl(\sum_{j \in \mathcal{N}_i} \alpha_{ij} \,W \mathbf{h}_j\Bigr)
$$
对$i$ 的邻居节点加权求和,再经过非线性激活函数得到新的特征表示
**多头注意力(隐藏层时拼接)**
如果有 $K$ 个独立的注意力头,每个头输出 $\mathbf{h}_i'^{(k)}$,则拼接后的输出为:
$$
\mathbf{h}_i' =
\big\Vert_{k=1}^K
\sigma\Bigl(\sum_{j \in \mathcal{N}_i} \alpha_{ij}^{(k)} \, W^{(k)} \mathbf{h}_j\Bigr)
$$
其中,$\big\Vert$ 表示向量拼接操作,$\alpha_{ij}^{(k)}$、$W^{(k)}$ 分别为第 $k$ 个注意力头对应的注意力系数和线性变换。
例假如:
$$
\mathbf{h}_i'^{(1)} = \sigma\left(\begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix}\right)
= \begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix}. \\
\mathbf{h}_i'^{(2)} = \sigma\left(\begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}\right)
= \begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}.
$$
将两个头的输出在特征维度上进行拼接,得到最终节点 $i$ 的新特征表示:
$$
\mathbf{h}_i' = \mathbf{h}_i'^{(1)} \,\Vert\, \mathbf{h}_i'^{(2)}
= \begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix} \,\Vert\, \begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}
= \begin{bmatrix} 0.6 \\ 0.4 \\ 0.6 \\ 1.4 \end{bmatrix}.
$$
**多头注意力(输出层时平均)**
在最终的输出层(例如分类层)通常会将多个头的结果做平均,而不是拼接:
$$
\mathbf{h}_i' =
\sigma\Bigl(
\frac{1}{K} \sum_{k=1}^K \sum_{j \in \mathcal{N}_i}
\alpha_{ij}^{(k)} \, W^{(k)} \mathbf{h}_j
\Bigr)
$$
## 直推式学习与归纳式学习
**直推式学习Transductive Learning**
模型直接在固定的训练图上学习节点的表示或标签,结果只能应用于这张图中的节点,无法直接推广到新的、未见过的节点或图。
例如DeepWalk ,它通过对固定图的随机游走生成节点序列来学习节点嵌入,因此只能得到训练图中已有节点的表示,一旦遇到新节点,需要重新训练或进行特殊处理。
**注意**GCN是直推式的因为它**依赖于整个图的归一化邻接矩阵**进行卷积操作,需要在固定图上训练。
**归纳式学习Inductive Learning**
模型学习的是一个映射函数或规则,可以将这种规则推广到未见过的**新节点**或**新图**上。这种方法能够处理动态变化的图结构或新的数据。
**例如:**
图神经网络的变体GAT都是归纳式的因为它们在聚合邻居信息时学习一个共享的函数该函数能够应用于任意新节点。
- 局部计算GAT 的注意力机制仅在每个节点的局部邻域内计算,不依赖于全局图结构。
- 参数共享:模型中每一层的参数(如 $W$ 和注意力参数 $\mathbf{a}$)是共享的,可以直接应用于新的、未见过的图。
**泛化到新节点**在许多推荐系统中如果有新用户加入新节点我们需要给他们做个性化推荐这就要求系统能够在不重新训练整个模型的情况下为新用户生成表示Embedding并且完成推荐预测。
**泛化到新图:** 分子图预测。我们会用一批训练分子(每个分子是一张图)来训练一个 GNN 模型,让它学会如何根据图结构与原子特征来预测分子的某些性质(如毒性、溶解度、活性等)。训练完成后,让它在新的分子上做预测。
## GNN的优点
**参数共享**

View File

@ -139,49 +139,95 @@ $$
3blue1brown 的讲解试图让我们从几何和线性映射的角度来理解点乘,而不仅仅是将它看作一系列数的乘加运算。下面详细说明这一点。
我们可以通过一个简单的例子来说明多头注意力拼接的计算过程。假设有两个注意力头($K=2$),且我们考虑一个节点 $i$ 以及它的两个邻居 $j=1$ 和 $j=2$。我们假设每个节点的输入特征 $\mathbf{h}_j$ 是一个二维向量,并且每个注意力头的线性变换 $W^{(k)}$ 将输入保持为二维(这里为了简化,取 $W^{(1)}$ 为单位矩阵,而 $W^{(2)}$ 为一个放大2倍的矩阵。另外我们假设各头对应的注意力权重归一化后的如下
假设有两个向量
- **注意力头1**
- $\alpha_{i1}^{(1)} = 0.6$
- $\alpha_{i2}^{(1)} = 0.4$
- **注意力头2**
- $\alpha_{i1}^{(2)} = 0.3$
- $\alpha_{i2}^{(2)} = 0.7$
同时设定节点特征为:
- $\mathbf{h}_1 = \begin{bmatrix} 1 \\ 0 \end{bmatrix}$
- $\mathbf{h}_2 = \begin{bmatrix} 0 \\ 1 \end{bmatrix}$
下面按照每个注意力头计算节点 $i$ 的新特征,再进行拼接:
---
### 注意力头 1 的计算
- **线性变换:**
$W^{(1)}$ 为单位矩阵,所以
$$
W^{(1)}\mathbf{h}_1 = \begin{bmatrix} 1 \\ 0 \end{bmatrix},\quad W^{(1)}\mathbf{h}_2 = \begin{bmatrix} 0 \\ 1 \end{bmatrix}.
$$
- **加权求和:**
对邻居加权求和得到
$$
\sum_{j\in\mathcal{N}_i} \alpha_{ij}^{(1)} \, W^{(1)}\mathbf{h}_j
= 0.6\begin{bmatrix} 1 \\ 0 \end{bmatrix} + 0.4\begin{bmatrix} 0 \\ 1 \end{bmatrix}
= \begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix}.
$$
- **非线性激活:**
假设激活函数 $\sigma$ 是 ReLU
$$
\mathbf{h}_i'^{(1)} = \sigma\left(\begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix}\right)
= \begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix}.
$$
---
### 注意力头 2 的计算
- **线性变换:**
假设 $W^{(2)}$ 为将输入放大2倍的矩阵
$$
W^{(2)} = \begin{bmatrix} 2 & 0 \\ 0 & 2 \end{bmatrix}.
$$
则有
$$
W^{(2)}\mathbf{h}_1 = \begin{bmatrix} 2 \\ 0 \end{bmatrix},\quad W^{(2)}\mathbf{h}_2 = \begin{bmatrix} 0 \\ 2 \end{bmatrix}.
$$
- **加权求和:**
使用注意力权重
$$
\sum_{j\in\mathcal{N}_i} \alpha_{ij}^{(2)} \, W^{(2)}\mathbf{h}_j
= 0.3\begin{bmatrix} 2 \\ 0 \end{bmatrix} + 0.7\begin{bmatrix} 0 \\ 2 \end{bmatrix}
= \begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}.
$$
- **非线性激活:**
同样使用 ReLU 激活
$$
\mathbf{h}_i'^{(2)} = \sigma\left(\begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}\right)
= \begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}.
$$
---
### 拼接最终输出
将两个头的输出在特征维度上进行拼接,得到最终节点 $i$ 的新特征表示:
$$
\mathbf{v} = \begin{bmatrix} v_1 \\ v_2 \end{bmatrix}, \quad \mathbf{w} = \begin{bmatrix} w_1 \\ w_2 \end{bmatrix}.
\mathbf{h}_i' = \mathbf{h}_i'^{(1)} \,\Vert\, \mathbf{h}_i'^{(2)}
= \begin{bmatrix} 0.6 \\ 0.4 \end{bmatrix} \,\Vert\, \begin{bmatrix} 0.6 \\ 1.4 \end{bmatrix}
= \begin{bmatrix} 0.6 \\ 0.4 \\ 0.6 \\ 1.4 \end{bmatrix}.
$$
传统上,点乘定义为
$$
\mathbf{v} \cdot \mathbf{w} = v_1w_1 + v_2w_2.
$$
---
3blue1brown 的观点是:
### 总结
- **将一个向量视为线性变换**
我们可以把 $\mathbf{v}$ 当作一个线性映射,它把任何向量 $\mathbf{w}$ 映射为一个实数,即
$$
T_{\mathbf{v}}(\mathbf{w}) = \mathbf{v}\cdot \mathbf{w}.
$$
这个映射 $T_{\mathbf{v}}$ 是一个**线性泛函**,它具有线性性:
$$
T_{\mathbf{v}}(a\mathbf{w}_1 + b\mathbf{w}_2) = aT_{\mathbf{v}}(\mathbf{w}_1) + bT_{\mathbf{v}}(\mathbf{w}_2).
$$
换句话说,$\mathbf{v}$ 变成了一个“工具”,通过这个工具我们可以“测量”任一向量在 $\mathbf{v}$ 方向上的分量大小。
- 每个注意力头独立计算:先用各自的线性变换 $W^{(k)}$ 对邻居节点特征进行转换,再用对应的注意力系数 $\alpha_{ij}^{(k)}$ 加权求和,最后经过非线性激活 $\sigma$ 得到输出 $\mathbf{h}_i'^{(k)}$。
- 最后将所有 $K$ 个头的输出通过拼接操作合并成最终的节点特征表示 $\mathbf{h}_i'$。
- **几何直观**
如果我们记 $\theta$ 为 $\mathbf{v}$ 和 $\mathbf{w}$ 之间的夹角,则点乘也可以写作
$$
\mathbf{v} \cdot \mathbf{w} = \|\mathbf{v}\|\|\mathbf{w}\|\cos\theta.
$$
这里,$\|\mathbf{w}\|\cos\theta$ 就是 $\mathbf{w}$ 在 $\mathbf{v}$ 方向上的投影长度。当我们用 $\|\mathbf{v}\|$ 乘上这个投影长度时,就得到了一个度量,这个度量告诉我们 $\mathbf{w}$ 在 $\mathbf{v}$ 方向上“有多大”的贡献。
- **矩阵乘法的视角**
我们也可以把点乘看作行向量和列向量的矩阵乘法:
$$
\mathbf{v}\cdot \mathbf{w} = \begin{bmatrix} v_1 & v_2 \end{bmatrix}\begin{bmatrix} w_1 \\ w_2 \end{bmatrix}.
$$
在这个表达式中,$\begin{bmatrix} v_1 & v_2 \end{bmatrix}$ 就相当于一个将二维向量映射到实数的线性变换,也正是我们上面定义的 $T_{\mathbf{v}}(\cdot)$。
总结来说3blue1brown 强调的点乘本质是:
- 把固定的向量 $\mathbf{v}$ 转换成一个线性映射(或线性泛函),这个映射作用在任意向量 $\mathbf{w}$ 上,返回一个标量;
- 这个标量不仅包含了 $\mathbf{w}$ 在 $\mathbf{v}$ 方向上的“投影”信息,而且反映了两者之间的对齐程度(通过余弦函数体现);
- 因此,点乘不仅仅是数值运算,而是一个把向量转换成测量工具,从而揭示向量间角度和方向关系的过程。
这个例子展示了多头注意力机制如何通过多个独立的注意力头捕捉不同的子空间特征,最后将它们合并形成更丰富的表示。