diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1960e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output/ \ No newline at end of file diff --git a/Java/力扣Hot 100题.md b/Java/力扣Hot 100题.md index cf1a673..051a89f 100644 --- a/Java/力扣Hot 100题.md +++ b/Java/力扣Hot 100题.md @@ -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 list1 = Arrays.asList(1, 2, 3); List list2 = Arrays.asList(1, 2, 3); List list3 = Arrays.asList(3, 2, 1); @@ -33,76 +32,6 @@ System.out.println(list1.equals(list3)); // false(顺序不同) ``` - - -要实现接口自定义排序,必须实现 `Comparator` 接口的 `compare(T o1, T o2)` 方法。 - -`Comparator` 接口中定义的 `compare(T o1, T o2)` 方法返回**一个整数**(非布尔值!!),这个整数的正负意义如下: - -- 如果返回负数,说明 `o1` 排在 `o2`前面。 -- 如果返回零,说明 `o1` 等于 `o2`。 -- 如果返回正数,说明 `o1` 排在 `o2`后面。 - -```text -public class TestComparator { - // 定义一个升序排序的 Comparator - static Comparator ascComparator = new Comparator() { - @Override - public int compare(Integer a, Integer b) { - return a - b; // 如果 a < b, 则返回负数 - } - }; - - public static void main(String[] args) { - // 创建一个整数列表 - List 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接口` - -```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)); - } - } -} - -``` - **逻辑比较** @@ -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,9 +132,9 @@ String sortedStr = new String(charArray); System.out.println("After removal: " + map); // 输出 {apple=10, banana=20} } } - ```text + ``` + - #### **`ArrayList`** @@ -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 = new ArrayList<>(); + + +for (List row : list) { + for (int num : row) { + System.out.print(num + " "); + } + System.out.println(); // 换行 +} +for (int i = 0; i < list.size(); i++) { + List 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 = new ArrayList<>(); - - -for (List row : list) { - for (int num : row) { - System.out.print(num + " "); - } - System.out.println(); // 换行 -} -for (int i = 0; i < list.size(); i++) { - List row = list.get(i); - for (int j = 0; j < row.size(); j++) { - System.out.print(row.get(j) + " "); - } - System.out.println(); // 换行 -} -```text - - - -**如果事先不知道数组的大小呢?** - -``` -List 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 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; @@ -642,7 +543,7 @@ public class DequeExample { public static void main(String[] args) { // 使用 LinkedList 实现双端队列 Deque deque = new LinkedList<>(); - + // 在队列头部添加元素 deque.addFirst(10); // 在队列尾部添加元素 @@ -651,27 +552,26 @@ public class DequeExample { deque.offerFirst(5); // 在队列尾部插入元素 deque.offerLast(30); - + System.out.println("双端队列内容:" + deque); - + // 查看队头和队尾元素,不移除 int first = deque.peekFirst(); int last = deque.peekLast(); System.out.println("队头元素:" + first); System.out.println("队尾元素:" + last); - + // 从队头移除元素 int removedFirst = deque.removeFirst(); System.out.println("移除队头元素:" + removedFirst); // 从队尾移除元素 int removedLast = deque.removeLast(); System.out.println("移除队尾元素:" + removedLast); - + 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 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` 接口的 `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 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接口` + ```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 的元素和,即 diff --git a/Java/招标文件解析.md b/Java/招标文件解析.md index 0f43ab2..abb55de 100644 --- a/Java/招标文件解析.md +++ b/Java/招标文件解析.md @@ -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结构介绍 -1 +1 @@ -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服务器对半,就是10;TPM无上限) -``` +```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) diff --git a/Java/苍穹外卖.md b/Java/苍穹外卖.md index 40d046e..1b554b2 100644 --- a/Java/苍穹外卖.md +++ b/Java/苍穹外卖.md @@ -532,8 +532,6 @@ JdkSerializationRedisSerializer,**导致我们存到Redis中后的数据和原 - - ### 功能测试 **通过RedisTemplate对象操作Redis** diff --git a/科研/图神经网络.md b/科研/图神经网络.md index 10c42b4..c7bea96 100644 --- a/科研/图神经网络.md +++ b/科研/图神经网络.md @@ -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的优点: **参数共享** diff --git a/科研/草稿.md b/科研/草稿.md index a3ff87c..c32f8c0 100644 --- a/科研/草稿.md +++ b/科研/草稿.md @@ -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}$ 方向上的“投影”信息,而且反映了两者之间的对齐程度(通过余弦函数体现); -- 因此,点乘不仅仅是数值运算,而是一个把向量转换成测量工具,从而揭示向量间角度和方向关系的过程。 +这个例子展示了多头注意力机制如何通过多个独立的注意力头捕捉不同的子空间特征,最后将它们合并形成更丰富的表示。