Showing preview only (990K chars total). Download the full file or copy to clipboard to get everything.
Repository: HarleysZhang/dl_note
Branch: main
Commit: ddc620777d8d
Files: 86
Total size: 604.7 KB
Directory structure:
gitextract_72z1ypi7/
├── .gitignore
├── 1-math_ml_basic/
│ ├── cpp_learn_xmind/
│ │ └── C++多线程-并发编程.xmind
│ ├── grpc基础笔记.md
│ ├── nlp背景知识总结.md
│ ├── python_learn_xmind/
│ │ ├── Python 编程笔记-面向对象高级编程.xmind
│ │ ├── Python数据结构和高级特性.xmind
│ │ ├── Python编程笔记- 函数式编程.xmind
│ │ └── Python编程笔记-面向对象基础编程.xmind
│ ├── rust编程基础.md
│ ├── src/
│ │ ├── cpp/
│ │ │ ├── binary_search.cpp
│ │ │ ├── class_copy_move.cpp
│ │ │ ├── cpp_basic.cpp
│ │ │ ├── fileWrapper.cpp
│ │ │ ├── multi_thread_demo.cpp
│ │ │ └── test_template.cpp
│ │ └── python/
│ │ ├── chatgpt_api_demo.py
│ │ ├── classifynet_torch_to_onnx.py
│ │ ├── conv_layer.py
│ │ ├── gd_double_variable.py
│ │ ├── gradio_demo.py
│ │ ├── multi_cal_tasks.py
│ │ ├── multi_io_tasks.py
│ │ ├── python_basic.py
│ │ └── thop_demo.py
│ ├── ssh远程登录服务.md
│ ├── transformers库快速入门.md
│ ├── 深度学习基础-机器学习基本原理.md
│ ├── 深度学习数学基础-概率与信息论.md
│ └── 随机梯度下降法的数学基础.md
├── 2-deep_learning_basic/
│ ├── cnn基础部件-BN层详解.md
│ ├── cnn基础部件-卷积层详解.md
│ ├── cnn基础部件-激活函数详解.md
│ ├── pytorch_basic/
│ │ ├── Pytorch基础-tensor数据结构.md
│ │ ├── Pytorch基础-张量数学运算.md
│ │ ├── Pytorch基础-张量结构操作.md
│ │ └── src/
│ │ ├── tensor_demo.py
│ │ └── tensor_math.py
│ ├── pytorch_code/
│ │ ├── pytorch-c10模块详解.md
│ │ ├── pytorch代码库结构拆解.md
│ │ ├── pytorch张量实现分析.md
│ │ ├── pytorch架构概览.md
│ │ └── pytorch编译流程解析.md
│ ├── 反向传播与梯度下降详解.md
│ ├── 深度学习基础-优化算法详解.md
│ ├── 深度学习基础-参数初始化详解.md
│ ├── 深度学习基础-损失函数详解.md
│ └── 深度学习基础总结.md
├── 3-classic_backbone/
│ ├── DenseNet论文解读.md
│ ├── ResNetv2论文解读.md
│ ├── ResNet网络详解.md
│ ├── densenet.py
│ ├── efficient_cnn/
│ │ ├── CSPNet论文详解.md
│ │ ├── MobileNetv1论文详解.md
│ │ ├── RepVGG论文详解.md
│ │ ├── ShuffleNetv2论文详解.md
│ │ ├── VoVNet论文解读.md
│ │ └── vovnet.py
│ ├── shufflenetv2_expr.py
│ └── 经典backbone总结.md
├── 4-deep_learning_alchemy/
│ ├── 深度学习炼丹-不平衡样本的处理.md
│ ├── 深度学习炼丹-数据增强.md
│ ├── 深度学习炼丹-数据标准化.md
│ ├── 深度学习炼丹-模型可视化.md
│ ├── 深度学习炼丹-正则化策略.md
│ └── 深度学习炼丹-超参数调整.md
├── 5-model_compression/
│ ├── README.md
│ ├── 基于pytorch实现模型剪枝.md
│ ├── 模型压缩-剪枝算法详解.md
│ ├── 模型压缩-知识蒸馏详解.md
│ ├── 模型压缩-神经网络量化基础.md
│ ├── 模型压缩-轻量化网络总结.md
│ └── 深度学习模型压缩方法概述.md
├── 6-model_deploy/
│ ├── AI芯片速览.md
│ ├── ONNX模型分析与使用.md
│ ├── TensorRT基础笔记.md
│ ├── ncnn源码解析-Net类.md
│ ├── ncnn源码解析-sample运行.md
│ ├── 卷积神经网络复杂度分析.md
│ ├── 模型压缩部署概述.md
│ └── 模型推理加速技巧-融合卷积和BN层.md
├── LICENSE
├── README.md
├── images/
│ └── dl/
│ └── courgette.log
├── process_image.py
├── 互联网技术大佬独立博客推荐.md
└── 手把手教你注册和使用ChatGPT.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
================================================
FILE: 1-math_ml_basic/grpc基础笔记.md
================================================
## 一,ProtoBuf 基础
在网络通信和通用数据交换等应用场景中经常使用的技术除了 `JSON` 和 `XML`,另外一个就是 `ProtoBuf`。protocol buffers (ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
我们可以通过 ProtoBuf 定义数据结构,然后通过 ProtoBuf 工具生成各种语言版本的数据结构类库,用于操作 ProtoBuf 协议数据。
### 1.1,ProtoBuf 例子
使用 `gRPC` 主要分为三步:
1. 编写 `.proto` `pb` 文件,制定通讯协议。
2. 利用对应插件将 `.proto` pb 文件编译成对应语言的代码。
3. 根据生成的代码编写业务代码。
例子,文件名: `response.proto`。通过 ProtoBuf 语法定义数据结构(消息),这些定义好的数据结构保存在 `.proto` 为后缀的文件中。
```protobuf
// 指定 protobuf 的版本,proto3 是最新的语法版本
syntax = "proto3";
// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Response {
string data = 1; // 定义一个string类型的字段,字段名字为data, 序号为1
int32 status = 2; // 定义一个int32类型的字段,字段名字为status, 序号为2
}
```
> 说明:我们通常将 protobuf 消息定义保存在 .proto 为后缀的文件中,字段后面的序号,不能重复,定义了就不能修改,可以理解成字段的唯一 ID。
**消息**(`message`),在 `protobuf` 中指的就是我们要定义的数据结构。在上面的例子中,我们定义了一个消息,名字为 `Response`,它有两个字段,一个是 `data`,一个是 `status`。`data` 字段的类型是 `string`,`status`` 字段的类型是 `int32`。
### 1.2,Protobuf 文件编译
从 .proto 文件生成了什么?
当用 protocol buffer 编译器来运行 .proto 文件时,编译器将生成所选择语言的代码,这些代码可以操作在 .proto 文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
对 Python 来说,Python 编译器为 .proto 文件中的每个消息类型生成一个含有**静态描述符**的模块,该模块与一个元类(`metaclass`)在运行时(`runtime`)被用来创建所需的 Python 数据访问类。
## 二,Python gRPC 基础
### 2.1,gRPC 定义
`gRPC` 是一种现代化开源的高性能 `RPC` 框架,能够运行于任意环境之中,最初由谷歌进行开发,它使用 `HTTP/2` 作为传输协议。
> RPC(Remote Procedure Call),即远程过程调用,主要是为了解决在分布式系统中,服务之间的调用问题。
`gRPC` 也是基于以下理念:**定义一个服务,指定其能够被远程调用的方法**(包含参数和返回类型),即在 `gRPC` 里,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。
- 在服务端,实现这个接口并且运行 `gRPC` 服务器来处理客户端调用。
- 在客户端,有一个 `stub ` (存根)提供和服务端相同的方法。

### 2.2,gRPC 优点
使用 gRPC, 我们**可以一次性的在一个 `.proto` 文件中定义服务并使用任何支持它的语言去实现客户端和服务端**,反过来,它们可以应用在各种场景中,从 Google 的服务器到你自己的平板电脑——gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。
> `gRPC` 默认使用的 `protocol buffers`,是 Google 开源的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做**数据存储**或 **RPC 数据交换**格式。
总结就是,**使用 gRPC 能让我们更容易编写跨语言的分布式代码**。
### 2.3,gRPC 开发步骤
### 2.3.1,编写 .proto 文件定义服务(Defining the service)
像许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用 protocol buffers 作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。
`proto` 文件主要三要素:**服务、方法、消息**。下面使用 protocol buffers 定义了一个 RouteGuide 服务的例子。
1,服务:
```protobuf
// 定义服务
service RouteGuide {
// (Method definitions not shown)
...
}
```
2,接下来,在服务定义内部定义 `rpc` **方法**,指定它们的请求和响应类型,所有这些方法都在 `RouteGuide` 服务中使用:
```protobuf
// 定义方法
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
```
3,`.proto` 文件还应包含服务方法中使用的所有请求和响应类型的协议缓冲**消息类型**定义,例如 `Point` 消息类型:
```protobuf
// 定义消息
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
```
完整的 `route_guide.proto` 文件如下:
```protobuf
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
option objc_class_prefix = "RTG";
package routeguide;
// Interface exported by the server.
service RouteGuide {
// A simple RPC.
//
// Obtains the feature at a given position.
//
// A feature with an empty name is returned if there's no feature at the given
// position.
rpc GetFeature(Point) returns (Feature) {}
// A server-to-client streaming RPC.
//
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// A client-to-server streaming RPC.
//
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// A Bidirectional streaming RPC.
//
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
// The name of the feature.
string name = 1;
// The point where the feature is detected.
Point location = 2;
}
// A RouteNote is a message sent while at a given point.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
```
值得注意的是, 在gRPC 中我们可以定义**四种**类型的服务方法。
1. **普通 rpc**: 客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样。
```protobuf
rpc SayHello(HelloRequest) returns (HelloResponse);
```
2. **服务器流式 rpc**: 其中客户端向服务器发送请求,并获得一个流来读取一系列消息。客户端从返回的流中读取,直到没有更多的消息。gRPC 保证在单个 RPC 调用中的消息是有序的。
```protobuf
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
```
3. **客户端流式 rpc**: 其中客户端写入一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端完成了消息的写入,它就等待服务器读取消息并返回响应。同样,gRPC 保证在单个 RPC 调用中对消息进行排序。
```protobuf
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
```
4. **双向流式 rpc**: 其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序读写: 例如,服务器可以等待接收所有客户端消息后再写响应,或者可以交替读取消息然后写入消息,或者其他读写组合。每个流中的消息是有序的。
### 2.2,生成指定语言的代码-生成客户端和服务器代码(Generating client and server code)
前面那节内容,我们知道了如何在 `proto` 文件中**定义服务**,接下来我们需要使用 `gRPC` 的协议编译器 `protoc` 从 `.proto` 文件中**生成客户端和服务器代码**:
- 在服务器端,**服务器实现服务声明的方法**,并运行一个 gRPC 服务器来处理客户端发来的调用请求。gRPC 底层会对传入的请求进行解码,执行被调用的服务方法,并对服务响应进行编码。
- 在客户端,客户端有一个称为存根(`stub`)的本地对象,它实现了与服务相同的方法。然后,客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的 protocol buffers 消息类型中——gRPC 在向服务器发送请求并返回服务器的 protocol buffers 响应之后进行处理。
```bash
# First, install the grpcio-tools package:
$ pip install grpcio-tools
# Use the following command to generate the Python code:
$ python -m grpc_tools.protoc -I../../protos --python_out=. --pyi_out=. --grpc_python_out=. ../../protos/route_guide.proto
```
**命令说明**:
- `-I`: proto 协议文件目录
- `--python_out` 和 `--grpc_python_out` 生成 python 代码的目录
- 命令最后面的参数是 `proto` 协议文件路径
命令执行后生成 route_guide_pb2.py 文件和 route_guide_pb2_grpc.py 文件。
- `route_guide_pb2.py`: 主要包含 proto 文件定义的消息类。
- `route_guide_pb2_grpc.py`: **包含服务端和客户端代码**,比如:
- `RouteGuideStub`,客户端可以使用它调用 RouteGuide RPCs
- `RouteGuideServicer`,定义 RouteGuide 服务的实现接口
- `add_RouteGuideServicer_to_server`: 将 route_guide.proto 中定义的服务的函数 RouteGuideServicer 添加到 grpc.Server
### 2.3,编写业务逻辑代码-创建和实现服务端(Implementing the server)
**gRPC 帮我们解决了 RPC 中的服务调用、数据传输以及消息编解码,我们剩下的工作就是要编写业务逻辑代码**。
而创建和运行 RouteGuide 服务器可分为两个主要步骤:
1. 根据前面由 `proto` 服务定义生成的**服务程序接口**,开始编写实际可运行的服务接口代码,注意是包含执行服务的**实际**“工作”的函数。
2. 运行一个 `gRPC` 服务器,以侦听客户端的请求并传输响应。
可以在 grpc 的仓库的 [examples/python/route_guide/route_guide_server.py](https://github.com/grpc/grpc/tree/master/examples/python/route_guide) 中找到示例 `RouteGuide` 服务器代码。
route_guide_server.py 有一个 RouteGuideServicer 类,它是生成的类 route_guide_pb2_grpc.RouteGuideServicer 的子类:
```python
# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
```

#### 2.3.1,响应流式 RPC(Response-streaming RPC)
```python
def ListFeatures(self, request, context):
left = min(request.lo.longitude, request.hi.longitude)
right = max(request.lo.longitude, request.hi.longitude)
top = max(request.lo.latitude, request.hi.latitude)
bottom = min(request.lo.latitude, request.hi.latitude)
for feature in self.db:
if (
feature.location.longitude >= left
and feature.location.longitude <= right
and feature.location.latitude >= bottom
and feature.location.latitude <= top
):
yield feature
```
"ListFeatures" 方法是实现 Protocol Buffer 文件中定义的 "ListFeatures" 服务器到客户端流式 RPC 的方法。它接受一个类型为 "Rectangle" 的请求对象和一个上下文对象作为参数。它计算给定矩形的边界框,并迭代 RouteGuide 数据库中的特征。对于每个落在边界框内的特征,它将特征发送给客户端。
在这里,request.lo.longitude 和 request.hi.longitude 分别表示矩形的左下角和右上角的经度。因为经度越往左越小,所以这行代码使用 min() 函数来计算这两个经度值中的最小值,从而得到矩形的左边界。
#### 2.3.2,双向流式 RPC(Bidirectional streaming RPC)
```python
def RouteChat(self, request_iterator, context):
prev_notes = []
for new_note in request_iterator:
for prev_note in prev_notes:
if prev_note.location == new_note.location:
yield prev_note
prev_notes.append(new_note)
```
`RouteChat` 方法在 Protocol Buffer 文件中被定义为一个双向流式 `RPC`。它接受一个流式的请求对象 `RouteNote ` 和一个上下文对象作为参数,并返回一个流式的响应对象 `RouteNote`。
#### 2.3.3,启动服务器(Starting the server)
实现所有 `RouteGuide` 方法后,下一步是启动 `gRPC` 服务器,以便客户端可以实际使用您的服务:
```python
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
RouteGuideServicer(), server
)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
```
server.start() 方法是非阻塞的。它会创建一个新的线程来处理请求。调用 server.start() 的线程通常在此期间不需要做其他工作。在这种情况下,您可以调用 server.wait_for_termination() 方法,以清晰地阻塞调用线程,直到服务器终止。
### 2.4,创建和实现客户端(Creating the client)
要调用服务方法,我们首先需要创建一个**存根**(stub)。 我们实例化从 ``.proto`` 生成的 `route_guide_pb2_grpc` 模块中的 `RouteGuideStub` 类。
```python
channel = grpc.insecure_channel('localhost:50051')
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
```
#### 2.4.1,调用服务方法
- 对于返回单个响应的 RPC 方法(“响应-单一”方法),gRPC Python 支持同步(阻塞)和异步(非阻塞)的控制流语义。
- 对于响应流式传输的 RPC 方法,调用会立即返回一个响应值的迭代器。对该迭代器调用 next() 方法会阻塞,直到从迭代器中产生的响应可用为止。
#### 2.4.2,简单 RPC
对于简单 RPC GetFeature 的同步调用几乎与调用本地方法一样简单。RPC 调用会等待服务器响应,然后将返回响应或引发异常:
```python
feature = stub.GetFeature(point)
```
### 2.4,运行客户端和服务器(Running the client and server)
```bash
# Run the server:
$ python route_guide_server.py
# From a different terminal, run the client:
$ python route_guide_client.py
```
## 三,python 协程
我们知道,函数(子程序)调用是通过**栈**实现的,子程序调用总是一个入口紧跟着一次返回,**调用顺序是明确的**。
而协程的调用和子程序不同,协程看上去也是子程序,但执行过程中,**在子程序内部可中断**,然后转而执行别的子程序,在**适当**的时候再返回来接着执行。注意,这里在一个子程序的内部中断,去执行其他子程序,不是函数调用,有点类似 CPU 的中断。
> 学习过单片机的应该能理解这里的中断的概念。
协程的特点在于是一个线程执行,和多线程比,最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换, 而是由程序自身控制,因此,没有线程切换的开销,**和多线程比,线程数量越多,协程的性能优势就越明显**。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
对于多核心 cpu,可以考虑使用多进程 + 协程的方式,充既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
### 3.1,python 协程实践
`asyncio` 是 Python 3.4 版本引入的标准库,直接内置了对异步 IO 的支持。
Python 对协程的支持是通过 generator 实现的。`yield from`(python3.5 版本之后是 `await`) 语法可以让我们方便地调用另一个 `generator`。
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过 锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始 执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
### 3.2,asyncio 库学习
`asyncio.run` 函数是 Python 3.7 版本引入的一个工具函数,用于运行一个异步函数并管理整个异步事件循环的生命周期。使用 asyncio.run 的典型场景是在脚本或应用程序的顶层部分运行一个异步函数,而不必手动管理事件循环的创建和关闭。
```python
import asyncio
async def my_async_function():
print("Running async function")
# asyncio.run 负责创建事件循环、运行 my_async_function 函数,并在函数执行完成后关闭事件循环。
asyncio.run(my_async_function())
```
### 3.3,grpc.aio.server 和 grpc.server 函数的区别
grpc.aio.server 和 grpc.server 都是 gRPC Python 库提供的服务器创建函数,但它们在处理异步请求和同步请求方面有所不同。
1. `grpc.aio.server`:
- grpc.aio.server 是基于 asyncio(异步 I/O)的 gRPC 服务器实现。
- 支持异步请求处理,适用于异步 Python 代码。
- 允许使用 async def 声明的异步处理函数。
- 通常与 asyncio.run 一起使用。
2. `grpc.server`:
- grpc.server 是传统的 gRPC 服务器实现,采用同步请求处理方式。
- 使用标准的 gRPC 处理函数,不支持异步处理。
- 适用于传统的同步 Python 代码
## 四,Typer 库基础
`Typer` 是一个用于构建 `CLI` 应用程序的库,它简化了 CLI 应用程序的创建和使用过程。它是基于 `Click` 库构建的,提供了更简单的 API。
> CLI 是 Command Line Interface 的缩写,中文翻译为命令行界面。它是一种通过纯文本命令进行交互的用户界面,用户通过键入命令来执行特定的操作。
安装方法:
```
pip install "typer[all]"
```
### 4.1,包含两个子命令的简单示例
以下是使用 `typer.Typer()` 创建一个带有**两个子命令**及其参数的 `CLI` 应用的基本示例:
```python
import typer
app = typer.Typer()
@app.command()
def hello(name: str):
print(f"Hello {name}")
@app.command()
def goodbye(name: str, formal: bool = False):
if formal:
print(f"Goodbye Ms. {name}. Have a good day.")
else:
print(f"Bye {name}!")
if __name__ == "__main__":
app()
```
程序运行结果:

## 参考资料
1. [ProtoBuf 快速入门教程](https://www.tizi365.com/archives/367.html)
2. [grpc-Basics tutorial](https://grpc.io/docs/languages/python/basics/#client)
3. [gRPC教程](https://www.liwenzhou.com/posts/Go/gRPC/)
================================================
FILE: 1-math_ml_basic/nlp背景知识总结.md
================================================
- [词向量](#词向量)
- [`One-Hot` 编码](#one-hot-编码)
- [Word Embedding](#word-embedding)
- [vocab 和 merge table](#vocab-和-merge-table)
- [Token ID 序列](#token-id-序列)
- [embedding 维度](#embedding-维度)
- [Word Embedding Vector](#word-embedding-vector)
- [nn.Linear](#nnlinear)
- [参考资料](#参考资料)
### 词向量
在 CV 领域,需要将数字图像转换为**矩阵/张量**进行神经网络计算;而在 NLP 领域,自然语言通常以文本形式存在,同样需要将文本数据转换为一系列的**数值**方便计算机进行计算,这里会涉及到**词向量**的概念,转换的方法通常有:
- `One-Hot` 编码: 一种简单的单词表示方式
- `Word Embedding`: 一种分布式单词表示方式
- `Word2Vec`: 一种词向量的训练方法
#### `One-Hot` 编码
`One-hot` 编码是一种很简单的将单词数值化的方式。对于单词数量为 N 的词表,则需用一个长度为 N 的向量表示一个单词,在这个向量中该单词对应的位置数值为1,其余单词对应的位置数值全部为0。举例如下:
**词典**: [queen, king, man, woman, boy, girl ]

上图展示了词典中 `6` 个单词的 one-hot 编码表示。虽然这个表示还是很简单的,但是其也存在以下问题:
- 现实当中单词数量往往有几十万甚至上百万,这样如果用 one-hot 编码的方式表示单词,其向量维度会很长,且极其稀疏,即**高维高稀疏**。
- 因为向量之间是正交且点积为 0,所以无法直接通过向量计算的方式来得出单词之间的关系,即**无法看出词之间的语义相似性**。
#### Word Embedding
Word Embedding 的概念。如果将 word 看作文本的最小单元,可以将 Word Embedding 理解为一种**映射**,其过程是:将文本空间中的某个 word,通过一定的方法,映射或者说嵌入(embedding)到另一个数值向量空间(之所以称之为 embedding,是因为这种表示方法往往伴随着一种**降维**的意思)
1,基于频率的 Word Embedding 又可细分为如下几种:
- Count Vector
- TF-IDF Vector
### vocab 和 merge table
在自然语言处理中,`Token` 是指一段文本中的基本单位,通常是一个词、一个词组或者一个字符。Tokenization 是将一段文本分解为一系列的 Token 的过程。
`vocab` 文件和 `merge table` 可以用来将原始文本分割成一系列的 `token`。
1,Vocab 文件,全称为 vocabulary file,是指包含了所有可能出现在文本中的 `token` 列表。在 LLM 中,每个 token 对应着一个编号(或者叫做词汇表中的索引),以便在模型中表示为对应的向量。Vocab 文件中的每个token 一般都是由一个或多个字符组成的,通常会包括汉字、英文单词、标点符号等。以下是一个示例vocab文件的部分内容:
```bash
csharpCopy code
[PAD]
[UNK]
[CLS]
[SEP]
[MASK]
我
的
你
是
了
...
```
在这个例子中,前五个 token 是特殊 token,分别表示填充、未知、开始、结束和掩码,其余的token是由汉字和英文单词组成的。在 LLM 中,所有输入文本的 token 都会映射为这个 vocab 文件中的其中一个 token。
2,`Merge Table` 是 LLM 中另一个重要的概念,它用于将文本中的字符逐步合并为**更大的 token**。Merge Table包含了一系列的合并操作,每个操作都是由两个字符组成的,表示将这两个字符合并为一个新的 token。
以下是一个示例的 Merge Table 的部分内容:
```bash
L U
K S
e r
o v
o l
oo o
```
在这个例子中,每个操作将两个字符合并为一个新的 token,例如将 L 和 U 合并为一个新的 token LU,将 e 和 r合并为一个 token `er` 等等。这些操作的顺序和次数都是由 LLM 模型自动学习得到的,通过这些合并操作,LLM可以将任意长度的文本逐步分割成一系列的 token,然后再进行模型预测。
### Token ID 序列
在自然语言处理中,计算机无法直接处理文本,需要将文本转换为数字形式进行处理。`Token ID` 序列就是这样一种**数字形式**,它可以被输入到神经网络或其他机器学习算法中进行训练和预测。
具体来说,`Token ID` 序列是指将原始文本序列中的每个单词或子词(`token`)映射为对应的 ID 后得到的**整数序列**。在使用 `tokenizer` 对文本进行编码时,tokenizer 会将每个单词或子词映射为一个唯一的 ID,得到 token ID 序列作为模型的输入。
### embedding 维度
在自然语言处理任务中,会将输入的文本序列通过 `embedding` 层**转换为固定维度的向量序列**,然后再通过后续的神经网络层进行处理。embedding 层的输出维度称为 embedding 维度(`embed_dim`),是一个超参数,需要根据具体任务和数据集进行调整。
### Word Embedding Vector
`Word Embedding Vector` 是将单词映射到向量空间的一种方式,它可以**将单词的语义信息转化为向量形式**,并且可以在向量空间中计算单词之间的相似性,简单理解就是,将单词从原始文本格式转换为神经网络可以理解和处理的数字格式的一种方式。在深度学习中,我们通常使用 Word2Vec、GloVe 或 FastText 等算法来生成单词的嵌入向量。
下面是一个简单的 Python 示例,用于使用 PyTorch 生成单词的嵌入向量:
```python
import torch
import torch.nn as nn
from torchtext.vocab import Vocab
from collections import Counter
text = "hello world hello" # 假设我们有一个文本
counter = Counter(text.split()) # 统计每个单词的出现次数
vocab = Vocab(counter) # 创建词汇表
# 构建嵌入层
vocab_size = 1000
embedding_dim = 100
embedding_layer = nn.Embedding(vocab_size, embedding_dim)
# 假设我们有一个输入单词列表,每个单词都是从词汇表中随机选择的
input_words = ["hello", "world", "this", "is", "a", "test"]
# 将单词转换为索引列表
word_indexes = [vocab.index(word) for word in input_words]
# 将索引列表转换为PyTorch张量
word_indexes_tensor = torch.LongTensor(word_indexes)
# 将索引列表输入嵌入层以获取嵌入向量
word_embeddings = embedding_layer(word_indexes_tensor)
# 输出嵌入向量
print(word_embeddings)
```
1. 首先通过 `Vocab` 和输入文本字符串创建词汇表 `vocab`,并通过 `nn.Embedding` 创建了一个大小为 $1000\times 100$ 的嵌入层 `embedding_layer`,表示**词汇表**中有 `1000` 个单词,每个单词用一个 `100` 维的向量表示。
2. 然后,我们创建一个输入单词列表 input_words,并将每个单词转换为词汇表中对应的索引,和将索引列表转换为 PyTorch 张量。
3. 最后将张量 `word_indexes_tensor` 输入到嵌入层中,以获取每个单词的 100 维嵌入向量。
值得注意的是,嵌入向量的大小和维度通常需要根据任务进行调整。通常,词汇表越大,嵌入向量的维度就越高。此外,**嵌入向量的大小通常需要与模型中的其他参数相匹配,例如隐藏层的大小和注意力头的数量**。
### nn.Linear
在 PyTorch 中,`nn.Linear` 是一个模块,它实现了一个全连接层,它将输入张量的每个元素都乘以一个可学习的权重矩阵,并加上一个可学习的偏置向量,最后输出一个新的张量。全连接层的数学表达式为:
$$
\text{output} = \text{input} \times \text{weight}^\text{T} + \text{bias} \nonumber
$$
`nn.Linear` 的构造函数如下:
```python
nn.Linear(in_features: int, out_features: int, bias: bool = True)
```
各参数解释如下:
- `in_features` 表示输入张量的特征数,也就是输入张量的最后一维的大小,
- `out_features` 表示输出张量的特征数,也就是输出张量的最后一维的大小,
- `bias` 是一个布尔值,表示是否添加偏置向量。如果 `bias` 为 `False`,则不会添加偏置向量。
下面是一个示例代码,可直接运行,其创建了一个输入张量,并通过 层进行维度的线性变换。
```python
import torch.nn as nn
import torch
import time
# 创建一个输入张量
input_tensor = torch.randn(10, 20)
# 创建一个 Linear 层对象,将输入维度从 20 变换到 30
linear_layer = nn.Linear(20, 30)
# 将输入张量传递给 Linear 层进行变换
start_time = time.time()
output_tensor = linear_layer(input_tensor)
end_time = time.time()
# 打印输出张量的形状
print(f"Output shape: {output_tensor.shape}") # Output shape: torch.Size([10, 30])
print(f"Time taken: {end_time - start_time:.6f} seconds") # Time taken: 0.002543 seconds
```
> 深度学习论文公式和代码可视化见 github 仓库 [annotated_deep_learning_paper_implementations](https://github.com/labmlai/annotated_deep_learning_paper_implementations)。
### 参考资料
- https://paddlepedia.readthedocs.io/en/latest/tutorials/sequence_model/word_representation/index.html
- [Word Embedding 編碼矩陣](https://medium.com/ml-note/word-embedding-3ca60663999d#id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImFjZGEzNjBmYjM2Y2QxNWZmODNhZjgzZTE3M2Y0N2ZmYzM2ZDExMWMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYmYiOjE2ODExOTMwMTUsImF1ZCI6IjIxNjI5NjAzNTgzNC1rMWs2cWUwNjBzMnRwMmEyamFtNGxqZGNtczAwc3R0Zy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNjQ1MDI4MjQ3OTk2MjkwMjczMiIsImVtYWlsIjoiemhnMTMyMTUwMkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXpwIjoiMjE2Mjk2MDM1ODM0LWsxazZxZTA2MHMydHAyYTJqYW00bGpkY21zMDBzdHRnLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwibmFtZSI6IueroOa0qumrmCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BR05teXhZbzFFVlgwV3Zfemdzb1VBRmVPMzE5X3dDUnV2VWNNaGVYQktJZ0lnPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6Iua0qumrmCIsImZhbWlseV9uYW1lIjoi56ugIiwiaWF0IjoxNjgxMTkzMzE1LCJleHAiOjE2ODExOTY5MTUsImp0aSI6IjYxMzhiZTlmMDNkNjZkMjhmYTNmMTA1ZmYwYTIzMmMwNTU1OTRkNjIifQ.Nxda3BGc_YjHzMAPwTTqTKayTUTgIPUGR2CzqTjNkbolF_JdRnse2TFlLGC2OIUV0Shy17Omyu-5c_JKYojI-XZkHX9UtyZ-qkbP_zZG5FPaq27N2IlOMIMFlR3FMnmGgOu0L3YjimR_mExO-ltP2j-WncJEstVTPXrpzIEeVjEAfzIdvClMyCidPBdKmE9qHczhUcXKWL4oJjg4qBsqoljAW6g2u1V4ENc-t5vBZT91BGbSBF5snSNJ9TyjK1PGyUxEzpJNZX4WSboWhf-H0rlch9jWJeFZMncTPP8_6UqsTJXPCEkd0ZvTjxKLNASdaL-dalX4r01w74ezqRfKwg)
================================================
FILE: 1-math_ml_basic/rust编程基础.md
================================================
## 认识 Cargo
`cargo` 是 `Rust` 的包管理工具,提供了从项目的建立、构建到测试、运行乃至部署的完整功能,与 `Rust` 语言及其编译器 `rustc` 紧密结合。
1,创建项目。
```bash
$ cargo new world_hello
$ cd world_hello
```
创建的项目结构:
```console
$ tree
.
├── .git
├── .gitignore
├── Cargo.toml
└── src
└── main.rs
```
2,两种方式可以运行项目:
- `cargo run release`(默认是 debug),相当于执行了两个命令 cargo build 编译项目,和 ./target/debug/world_hello。
- 手动编译和运行项目。
3,`Cargo.toml` 和 `Cargo.lock`
- `Cargo.toml` 是一种轻量级的配置文件格式,用于配置项目的元信息、依赖关系、构建选项等。
- `Cargo.lock` 文件是 `cargo` 工具根据同一项目的 `toml` 文件生成的项目依赖详细清单,因此我们一般不用修改它。
## 一,宏
1, `Package` 是一个项目工程,而包只是一个编译单元,`src/main.rs` 和 `src/lib.rs` 都是编译单元,因此它们都是包。
2, 一个 `crate` 可以是二进制(`src/main.rs`)或者库(`src/lib.rs`),每一个包(crate)都有包跟( crate root),例如二进制包的包根是 `src/main.rs`,库包的包根是 `src/lib.rs`。它是编译器开始处理源代码文件的地方,同时也是包模块树的根部。
3,`rust` 出于安全考虑,默认情况下,所有的类型都是私有化的,包括函数、方法、结构体、枚举、常量,是的,就连模块本身也是私有化的。在 Rust 中,**父模块完全无法访问子模块中的私有项**,反之,子模块可以访问父模块。
4,模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为 `pub`。
5,使用 `super` 引用模块,`super` 代表的是父模块为开始的引用方式。
```rust
fn serve_order() {}
// 厨房模块
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order(); // 调用了父模块(包根)中的 serve_order 函数
}
fn cook_order() {}
}
```
### 1.1,属性
可以使用称被为宏的自定义句法形式来扩展 Rust 的功能和句法。宏需要被命名,并通过一致的句法去调用:`some_extension!(...)`。
定义新宏有两种方式:
- [声明宏(Macros by Example)](https://rustwiki.org/zh-CN/reference/macros-by-example.html)以更高级别的声明性的方式定义了一套新句法规则。
- [过程宏(Procedural Macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html)可用于实现自定义派生。
#### 1.1.1,过程宏
*过程宏*允许在执行函数时创建句法扩展。过程宏有三种形式:
- [类函数宏(function-like macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#function-like-procedural-macros) - `custom!(...)`
- [派生宏(derive macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#derive-macros)- `#[derive(CustomDerive)]`
- [属性宏(attribute macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#attribute-macros) - `#[CustomAttribute]`
1,**派生宏**
派生宏为派生(derive)属性定义新输入。这类宏在给定输入结构体(struct)、枚举(enum)或联合体(union) token流的情况下创建新程序项。它们也可以定义派生宏辅助属性。
2,**属性宏**
属性宏定义可以附加到程序项上的新的外部属性,这些程序项包括外部(extern)块、固有实现、trate实现,以及 trait声明中的各类程序项。
### 1.2,使用 tracing 记录日志
1,**在于日志只能针对某个时间点进行记录,缺乏上下文信息,而线程间的执行顺序又是不确定的,因此日志就有些无能为力**。而 `tracing` 为了解决这个问题,引入了 `span` 的概念( 这个概念也来自于分布式追踪 ),一个 `span` 代表了一个时间段,拥有开始和结束时间,在此期间的所有类型数据、结构化数据、文本数据都可以记录其中。
2,tracing` 中最重要的三个概念是 ` `Span` 、` Event ` 和 `Collector`。
#### 1.2.1,使用方法-span! 宏
`span!` 宏可以用于创建一个 `Span` 结构体,然后通过调用结构体的 `enter` 方法来开始,再通过超出作用域时的 `drop` 来结束。
#### 1.2.2,使用方法-#[instrument]
如果想要将某个函数的整个函数体都设置为 `span` 的范围,最简单的方法就是为函数标记上 `#[instrument]`,此时 tracing 会自动为函数创建一个 span,span 名跟函数名相同,在输出的信息中还会自动带上函数参数。
## 二,模式匹配
match分支匹配的用法非常灵活,它的基本语法为:
```rust
match VALUE {
PATTERN1 => EXPRESSION1,
PATTERN2 => EXPRESSION2,
PATTERN3 => EXPRESSION3,
}
```
`match` 匹配的通用形式:
```rust
match target {
模式1 => 表达式1,
模式2 => {
语句1;
语句2;
表达式2
},
_ => 表达式3
}
```
**match** 支持两种匹配模式(不可反驳的模式(irrefutable) 和可反驳的的模式(refutable)):
- 当明确给出分支的Pattern时,必须是可反驳模式,这些模式允许匹配失败
- 使用`_`作为最后一个分支时,是不可反驳模式,它一定会匹配成功
- 如果只有一个Pattern分支,则可以是不可反驳模式,也可以是可反驳模式
### 2.1,全模式列表
1,匹配字面值
2,匹配命名变量
3,单分支多模式
4,通过序列 ..= 匹配值的范围
5,解构并分解值: 使用模式来解构结构体、枚举、元组、数组和引用。
模式匹配一样要类型相同。
## 三,复合类型
### 3.1,结构体
结构体和元组类似,都是由多种类型组合而成。示例:
```rust
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
```
该结构体名称是 `User`,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 `username` 代表了用户名,是一个可变的 `String` 类型。
## 四,难点语法速记
1,Option 代表可能为空可能有值的一种类型,本质上是一个枚举,有两种分别是 Some 和 None。Some 代表有值,None 则类似于 null,代表无值。
2,Result 直接翻译过来就是“结果”,想象一下,我们的接口,服务有非常常规的调用场景,正常返回值,异常返回错误或抛异常等等。而 Rust 里就定义有了一个 Result 用于此场景。Result 内部本质又是一个枚举,内部分别是 Ok 和 Err,是 Ok 时则代表正常返回值,Err 则代表异常。
3,使用 ? 后,你不需要挨个判断并返回,任何一个 ? 返回 Err 了函数都会直接返回 Err。
4,unwrap 和 Option 的一样,正常则拿值,异常则 panic!
## 参考资料
- [rust入门秘籍-模式匹配的基本使用](https://rust-book.junmajinlong.com/ch10/01_pattern_match_basis.html)
- https://blog.vgot.net/archives/rust-some.html
================================================
FILE: 1-math_ml_basic/src/cpp/binary_search.cpp
================================================
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;
int binary_search(std::vector<int> arr, int target) {
int left = 0;
int right = arr.size() - 1;
while (left <= right) {
auto mid = (left + right) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
right = mid - 1;
else
left = left + 1;
}
return -1;
}
int main() {
std::vector<int> arr({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
int target = 5;
auto index = binary_search(arr, target);
std::cout << "index: " << index << std::endl;
}
================================================
FILE: 1-math_ml_basic/src/cpp/class_copy_move.cpp
================================================
#include <iostream>
class Complex
{
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符,用于把两个 Complex 对象相加
// 返回类型 函数名(参数列表) const;
// 用于声明这个函数是一个常量成员函数,不能修改成员变量
Complex operator+(const Complex& other) const
{
return Complex(real + other.real, imag + other.imag);
}
// 重载 - 运算符,用于把两个 Complex 对象相减
Complex operator-(const Complex& other) const
{
return Complex(real - other.real, imag - other.imag);
}
// 定义为友元函数,并重载 << 运算符
friend std::ostream& operator<<(std::ostream& os, const Complex& c)
{
os << "(" << c.real << "+" << c.imag << "i)";
return os;
}
private:
double real, imag;
};
int main()
{
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;
Complex c4 = c1 - c2;
std::cout << "c1 is " << c1 << std::endl;
std::cout << "c2 is " << c2 << std::endl;
std::cout << "c1 + c2 result is " << c3 << std::endl;
return 0;
}
================================================
FILE: 1-math_ml_basic/src/cpp/cpp_basic.cpp
================================================
#include <iostream>
#include <vector>
#include <string>
class MyClass {
public:
MyClass() { std::cout << "Default constructor" << std::endl; }
MyClass(const MyClass& other) { std::cout << "Copy constructor" << std::endl; }
MyClass(MyClass&& other) { std::cout << "Move constructor" << std::endl; }
};
int main() {
std::vector<MyClass> v1;
// 添加对象
v1.push_back(MyClass());
v1.push_back(MyClass());
std::cout << "Before move: " << std::endl;
// 打印容器中的对象数量
std::cout << "Size: " << v1.size() << std::endl;
// 移动容器中的对象
std::vector<MyClass> v2(std::move(v1));
std::cout << "After move: " << std::endl;
// 打印容器中的对象数量
std::cout << "v1 size: " << v1.size() << std::endl; // v1已被移动,不再包含任何元素
std::cout << "v2 size: " << v2.size() << std::endl; // v2包含了v1中的元素
return 0;
}
================================================
FILE: 1-math_ml_basic/src/cpp/fileWrapper.cpp
================================================
#include <stdio.h>
#include <iostream>
#include <fstream>
#include <string>
class fileWrapper {
public:
// 构造函数 打开文件
explicit fileWrapper(const std::string& filename) :
m_file(filename, std::ios::in | std::ios::out | std::ios::app) {
if (!m_file.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
}
// 析构函数 关闭文件
~fileWrapper() {
if (m_file.is_open()) {
throw std::runtime_error("File is need to close");
m_file.close();
}
}
void writeLine(const std::string& line) {
if (!m_file.is_open()) {
throw std::runtime_error("Failed to write file, file is close.");
}
m_file << line << std::endl;
}
private:
std::fstream m_file;
};
template <typename T>
void wrapper(T&& val) {
// 完美转发
std::cout << "input val is " << std::forward<T>(val) << std::endl;
}
int main() {
int x = 11;
wrapper(67);
wrapper("hello world");
wrapper(x);
try {
fileWrapper fw("test.txt");
fw.writeLine("Hello World");
}
// 离开作用域自动调用 ~FileWrapper(),关闭文件
catch(const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
================================================
FILE: 1-math_ml_basic/src/cpp/multi_thread_demo.cpp
================================================
#include <stdio.h>
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
// 线程函数
void worker() {
std::cout << "Hello from worker thread " << std::endl;
}
// 线程函数对象,但是声明为只调用一次
void simple_do_once(std::once_flag* flag)
{
std::call_once(*flag, [](){ std::cout << "Simple example: called once\n"; });
}
int main() {
std::once_flag flag1;
std::thread worker_thread(worker);
worker_thread.join();
std::thread thread_once(simple_do_once, &flag1);
if (thread_once.joinable())
thread_once.join();
return 0;
}
================================================
FILE: 1-math_ml_basic/src/cpp/test_template.cpp
================================================
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
#include <cassert>
using namespace std;
// 通过枚举类定义设备/后端类型
enum class Device {
CPU,
GPU,
NPU
};
// 1. 先声明一个通用的模板类 Tensor,形参包括 “后端(Device)” 和 “数据类型(typename T)”
template<Device Dev, typename T>
class Tensor;
// 2. 通过偏特化:Tensor<CPU, T>,实现 CPU 后端的 Tensor 类设计
/*特例化哪个参数,哪个参数就不用定义在 template<> 中*/
template<typename T>
class Tensor<Device::CPU, T> {
public:
// 显式构造函数: 传入 vector 类型张量形状参数
explicit Tensor(const vector<size_t> shape): shape(shape) {
size = 1;
for (auto dim_size:shape) {
size *= dim_size;
}
data.resize(size);
}
// 简单的张量索引访问接口
const T& operator[](size_t index) const {
return data[index];
}
// const 表示这是一个常量成员函数
size_t size() const noexcept {
return size;
}
const std::vector<size_t>& shape() const { return shape; }
// 张量的一些成员函数
void add(const Tensor<Device::CPU, T> input_t) {
assert size == input_t.size();
for (int i=0; i < size; i++) {
data[i] += input_t.data[i];
}
}
// Debug: 打印部分数据
void printDebug(const std::string& name) const {
std::cout << "[CPU Tensor<" << typeid(T).name() << ">] "
<< name << " shape: [";
for (auto s : shape) std::cout << s << " ";
std::cout << "], data[0] = " << data[0] << " ...\n";
}
private:
size_t size;
std::vector<size_t> shape;
std::vector<size_t> data;
};
template<typename T1,typename T2>
auto func(const T1& x, const T2& y) {
return x + y;
}
// 模板全特化
template<>
auto func(const int& x, const double& y) {
return x-y;
}
int main() {
auto result1 = add(3, 5);
auto result2 = add(8.7, 9.0);
std::cout << "result1: " << result1 << std::endl;
std::cout << "result2: " << result2 << std::endl;
}
================================================
FILE: 1-math_ml_basic/src/python/chatgpt_api_demo.py
================================================
import openai
# After set the API key, the code can run!
openai.api_key = "YOUR_API_KEY"
# Define the model and prompt
model_engine = "text-davinci-003"
prompt = "如何使用openai 官网的 chatgpt 服务,给出详细步骤"
# Generate a response
completion = openai.Completion.create(
engine=model_engine,
prompt=prompt,
max_tokens=1024,
n=1,
stop=None,
temperature=0.5,
)
# Get the response text
message = completion.choices[0].text
print(message)
================================================
FILE: 1-math_ml_basic/src/python/classifynet_torch_to_onnx.py
================================================
# -*- coding : utf-8 -*-
# author : honggao.zhang
# Create : 2021-2-20
# Update : 2021-3-12
# Version : 0.1.0
# Description : 1, Classify net pytorch convert to onnx model template program.
# 2, Support alxnet、inceptionv3、resnet、shufflenetv2 and so on.
import sys, os,argparse
import os.path as osp
current_dir = osp.abspath(os.path.dirname(__file__))
import numpy as np
from torchsummary import summary
from thop import profile
import torch
from torch.autograd import Variable
# Gets the GPU if there is one, otherwise the cpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu");print(device)
input_shape = [1, 3, 224, 224]
model_file_name = 'ResNet50_with_mask_sparsity_50.pth' # 模型文件
base_name = osp.splitext(model_file_name)[0]
current_dir = os.path.abspath(os.path.dirname(__file__))
weight_path = osp.join(current_dir, '../weights/', model_file_name); print(weight_path)
####################################################################################################################
def arg_parse():
parser = argparse.ArgumentParser()
parser.add_argument('--model', '-m', default = 'shufflenetv2_x1.0')
parser.add_argument('--weight', '-w', default = None)
parser.add_argument('--gpu', '-gpu', action = 'store_false')
args = parser.parse_args()
return args
###################################################################################################
def model_inference(net, input_data, weight_path):
"""Load model weight file and inference model."""
# net = torch.load(model_path).to(device)
if weight_path is not None:
weight_path_name = osp.splitext(weight_path)[0]
state_dict = torch.load(weight_path, map_location=lambda storage, loc: storage).to(device)
# net.load_state_dict(state_dict, strict = False)
print("Load model weight file %s success!" % weight_path_name)
else:
print("There is no model weights file!")
net.eval()
with torch.no_grad():
output = net(input_data)
print("Model output type and shape is ", type(output), output.shape) # torch.Size([1, 13, 48, 80])
assert list(output.shape) == [1, 1000]
return net
def torch_to_onnx(torch_model, input_data, onnx_file_path):
torch.onnx.export(torch_model,
input_data,
onnx_file_path,
verbose=False,
opset_version=9,
do_constant_folding=True, # 是否执行常量折叠优化
input_names=["input"], # 输入名
output_names=["output"], # 输出名
)
####################################################################################################################
def define_net():
pass
####################################################################################################################
def main(net_torch, net_name, input_shape):
# 1, Define model structure
net = net_torch
# 2, Define model input
input_data = Variable(torch.ones(input_shape)).to(device)
# 3, Start model inference
net = model_inference(net, input_data, None)
# 4, Convert torch model to onnx model
onnx_file_path = osp.abspath(osp.join(current_dir, '.../data/onnx_model/classifynet', net_name+".onnx")) # onnx 模型权重文件路径定义
torch_to_onnx(net, input_data, onnx_file_path)
# 5, Analysis model FLOPs
print(tuple(input_shape[1:]))
summary(net, tuple(input_shape[1:]))
print(input_data.shape)
macs, params = profile(net, inputs=(input_data, ))
print("Sum of model ops andparams is", (macs, params))
print("Convert and analysis model success!")
###################################################################################################
if __name__ == '__main__':
args = arg_parse()
if args.model == 'alexnet': # Alexnet example
net_name = 'alexnet'
from torchvision.models.alexnet import alexnet
net_torch = alexnet(True).eval()
elif args.model == 'resnet18': # ResNet example
net_name = 'resnet18'
from torchvision.models.resnet import resnet18
net_torch = resnet18(True).eval()
elif args.model=='inception_v3': # Inception_v3 example
net_name = 'inception_v3'
from torchvision.models.inception import inception_v3
net_torch = inception_v3(True, transform_input=False).eval()
elif args.model == 'vgg16': # VGG19 example
net_name = 'vgg16'
from torchvision.models.vgg import vgg16
net_torch = vgg16(True).eval()
elif args.model == 'densenet121':
net_name = 'densenet121'
from torchvision.models.densenet import *
net_torch = densenet121(True).eval()
elif args.model == 'MobileNetV2':
net_name = 'MobileNetV2'
from torchvision.models.mobilenet import mobilenet_v2
net_torch = mobilenet_v2(True).eval()
elif args.model == 'shufflenetv2_x1.0':
net_name = 'shufflenetv2_x1.0'
from torchvision.models.shufflenetv2 import shufflenet_v2_x1_0
net_torch = shufflenet_v2_x1_0(False).eval()
else:
raise NotImplementedError()
if args.gpu:
net_torch.to(device)
main(net_torch, net_name, input_shape)
================================================
FILE: 1-math_ml_basic/src/python/conv_layer.py
================================================
# -*- coding : utf-8 -*-
# Author: honggao.zhang + chatgpt
import torch
import time
import numpy as np
import time
class Conv2D:
def __init__(self, input_channels, output_channels, kernel_size):
self.input_channels = input_channels
self.output_channels = output_channels
self.kernel_size = kernel_size
self.weights = np.random.randn(output_channels, input_channels, kernel_size, kernel_size)
self.bias = np.zeros((output_channels, 1))
def forward(self, input):
batch_size, input_channels, height, width = input.shape
padded_input = np.pad(input, ((0, 0), (0, 0), (self.kernel_size // 2, self.kernel_size // 2),
(self.kernel_size // 2, self.kernel_size // 2)), mode='constant')
output = np.zeros((batch_size, self.output_channels, height, width))
for b in range(batch_size):
for oc in range(self.output_channels):
for r in range(height):
for c in range(width):
# kernel 矩阵和 input 矩阵的, 默认 array1*array2 就是对应元素的乘积
padded_input[b, :, r: r+self.kernel_size, c: c+self.kernel_size] * self.weights[oc, :, :, :]
output[b, oc, r, c] = np.sum(
padded_input[b, :, r:r+self.kernel_size, c:c+self.kernel_size]
* self.weights[oc]) + self.bias[oc]
return output
stride = 1
kernel_size = 3
for bs in range(batch_size):
for oc in range(output_channels):
output[bs, oc, oh, ow] += bias[oc]
for ic in range(input_channels):
for oh in range(height):
for ow in range(width):
for kh in range(kernel_size):
for kw in range(kernel_size):
output[bs, oc, oh, ow] += input[bs, ic, oh+kh, ow+kw] * weights[oc, ic, kh, kw]
batch_size = 32
input_channels = 3
output_channels = 16
height = 224
width = 224
kernel_size = 3
input = np.random.randn(batch_size, input_channels, height, width)
conv = Conv2D(input_channels, output_channels, kernel_size)
start_time = time.time()
output = conv.forward(input)
end_time = time.time()
print(f"Output shape: {output.shape}")
print(f"Time taken: {end_time - start_time:.6f} seconds")
class Conv2D(torch.nn.Module):
def __init__(self, input_channels, output_channels, kernel_size):
super().__init__()
self.conv = torch.nn.Conv2d(input_channels, output_channels, kernel_size, padding=kernel_size//2)
def forward(self, x):
return self.conv(x)
batch_size = 32
input_channels = 3
output_channels = 16
height = 224
width = 224
kernel_size = 3
input = torch.randn(batch_size, input_channels, height, width)
conv = Conv2D(input_channels, output_channels, kernel_size)
start_time = time.time()
output = conv(input)
end_time = time.time()
print(f"Output shape: {output.shape}")
print(f"Time taken: {end_time - start_time:.6f} seconds")
================================================
FILE: 1-math_ml_basic/src/python/gd_double_variable.py
================================================
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def target_function(x,y):
J = pow(x, 2) + 2*pow(y, 2)
return J
def derivative_function(theta):
x = theta[0]
y = theta[1]
return np.array([2*x, 4*pow(y, 1)])
def show_3d_surface(x, y, z):
fig = plt.figure()
ax = Axes3D(fig)
u = np.linspace(-3, 3, 100)
v = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(u, v)
R = np.zeros((len(u), len(v)))
for i in range(len(u)):
for j in range(len(v)):
R[i, j] = pow(X[i, j], 2)+ 4*pow(Y[i, j], 2)
ax.plot_surface(X, Y, R, cmap='rainbow')
plt.plot(x, y, z, c='black', linewidth=1.5, marker='o', linestyle='solid')
plt.show()
if __name__ == '__main__':
theta = np.array([-3, -3]) # 输入为双变量
eta = 0.1 # 学习率
error = 5e-3 # 迭代终止条件,目标函数值 < error
X = []
Y = []
Z = []
for i in range(50):
print(theta)
x = theta[0]
y = theta[1]
z = target_function(x,y)
X.append(x)
Y.append(y)
Z.append(z)
print("%d: x=%f, y=%f, z=%f" % (i,x,y,z))
d_theta = derivative_function(theta)
print(" ", d_theta)
theta = theta - eta * d_theta
if z < error:
break
show_3d_surface(X,Y,Z)
================================================
FILE: 1-math_ml_basic/src/python/gradio_demo.py
================================================
import gradio as gr
import random
import time
with gr.Blocks() as demo:
chatbot = gr.Chatbot()
msg = gr.Textbox()
clear = gr.Button("Clear")
def user(user_message, history):
return "", history + [[user_message, None]]
def bot(history):
bot_message = random.choice(["Yes", "No"])
history[-1][1] = bot_message
time.sleep(1)
return history
msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
bot, chatbot, chatbot
)
clear.click(lambda: None, None, chatbot, queue=False)
if __name__ == "__main__":
demo.launch()
================================================
FILE: 1-math_ml_basic/src/python/multi_cal_tasks.py
================================================
# -*- coding : utf-8 -*-
# Author: honggao.zhang + chatgpt
# Create: 2023-03-07
# Version : 0.1.0
# Description: 矩阵计算的 cpu 型密集任务,单进程、多进程和进程池创建指定数量多进程的性能对比
import threading, multiprocessing
import time
import numpy as np
import concurrent.futures
# 进程数等于CPU核心数
NUM_PROCESS = multiprocessing.cpu_count()
def timer(func):
"""decorator: print the cost time of run function"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')
return result
return wrapper
@timer
def matrix_mult(A, B):
if A.shape[1] != B.shape[0]:
raise ValueError("Matrix dimensions do not match.")
C = np.zeros((A.shape[0], B.shape[1]))
for i in range(A.shape[0]):
for j in range(B.shape[1]):
for k in range(A.shape[1]):
C[i][j] += A[i][k] * B[k][j]
return C
@timer
def matrix_mult_threaded(A, B, num_threads):
if A.shape[1] != B.shape[0]:
raise ValueError("Matrix dimensions do not match.")
C = np.zeros((A.shape[0], B.shape[1]))
def multiply(i_start, i_end):
for i in range(i_start, i_end):
for j in range(B.shape[1]):
for k in range(A.shape[1]):
C[i][j] += A[i][k] * B[k][j]
futures = []
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
chunk_size = A.shape[0] // num_threads
for i in range(num_threads):
i_start = i * chunk_size
i_end = (i+1) * chunk_size if i != num_threads-1 else A.shape[0]
futures.append(executor.submit(multiply, i_start, i_end))
concurrent.futures.wait(futures)
return C
@timer
def matrix_mult_multiprocess(A, B, num_processes):
"""matrix_mult_multiprocess 是主进程,
并通过 multiprocessing.Pool 进程池的方式批量创建子进程。"""
if A.shape[1] != B.shape[0]:
raise ValueError("Matrix dimensions do not match.")
C = np.zeros((A.shape[0], B.shape[1]))
def multiply(i_start, i_end):
for i in range(i_start, i_end):
for j in range(B.shape[1]):
for k in range(A.shape[1]):
C[i][j] += A[i][k] * B[k][j]
# Pool 类创建一个进程池,大小默认为 CPU 的核数。
p = multiprocessing.Pool()
for i in range(num_processes):
i_start = i * (A.shape[0] // num_processes)
i_end = (i+1) * (A.shape[0] // num_processes) if i != num_processes-1 else A.shape[0]
p.apply_async(multiply, args=(i_start, i_end))
# print('Waiting for all subprocesses done...')
p.close()
p.join()
return C
if __name__ == "__main__":
# 测试
A = np.random.rand(200, 200)
B = np.random.rand(200, 200)
C = matrix_mult(A, B)
C = matrix_mult_threaded(A, B, NUM_PROCESS)
C = matrix_mult_multiprocess(A, B, NUM_PROCESS)
================================================
FILE: 1-math_ml_basic/src/python/multi_io_tasks.py
================================================
# -*- coding : utf-8 -*-
# Author: honggao.zhang + chatgpt
# Create: 2023-03-07
# Version : 0.1.0
# Description: 下载网页图片的 I/O 型密集任务,单线程、多线程和多进程的性能对比
import time, os
import threading, multiprocessing
import queue
import requests
# https://picsum.photos/ 网站只需在我们的网址后添加您想要的图片尺寸(宽度和高度),就会得到一张随机图片。
# 图片的URL列表
IMAGE_URLS = [
"https://picsum.photos/id/1/200/300",
"https://picsum.photos/id/2/200/300",
"https://picsum.photos/id/3/200/300",
"https://picsum.photos/id/4/200/300",
"https://picsum.photos/id/5/200/300",
"https://picsum.photos/id/6/1024/1024",
"https://picsum.photos/id/7/1024/1024",
"https://picsum.photos/id/8/1024/1024",
"https://picsum.photos/id/9/1024/1024",
"https://picsum.photos/id/10/1024/1024",
]
# 存储图片的目录
SAVE_DIR = "images/download_images"
# 线程数
THREAD_POOL_SIZE = multiprocessing.cpu_count()
# 进程数等于CPU核心数
NUM_PROCESS = multiprocessing.cpu_count()
def timer(func):
"""decorator: print the cost time of run function"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')
return result
return wrapper
# 创建存储图片的目录
def create_dir(dir_name):
if not os.path.exists(dir_name):
os.makedirs(dir_name)
# 下载图片的函数
def download_image(url, save_path):
response = requests.get(url)
with open(save_path, "wb") as f:
f.write(response.content)
# 单线程下载网页图片
@timer
def single_thread_download():
dir_name = SAVE_DIR + "_sig_thread"
create_dir(dir_name)
for i, url in enumerate(IMAGE_URLS):
save_path = os.path.join(dir_name, f"image{i+1}.jpg")
download_image(url, save_path)
@timer
def multi_thread_download():
"""线程数不固定取决于输入图片列表大小
"""
dir_name = SAVE_DIR + "_muti_thread"
create_dir(dir_name)
threads = []
for i, url in enumerate(IMAGE_URLS):
save_path = os.path.join(dir_name, f"image{i+1}.jpg")
t = threading.Thread(target=download_image, args=(url, save_path))
threads.append(t)
t.start()
# 等待所有线程执行完毕
for t in threads:
t.join()
def thread_worker(work_queue, dir_name):
while not work_queue.empty():
try:
url = work_queue.get(block=False) # 非阻塞取数据
except queue.Empty:
break
else:
save_path = os.path.join(dir_name, "image{}.jpg".format(url.split('/')[-3]))
download_image(url, save_path)
# 表示前面排队的任务已经被完成,被队列的消费者线程使用
work_queue.task_done()
@timer
def thread_pool_download():
# queue 模块实现了多生产者、多消费者队列。适用于消息必须安全地在多线程间交换的多线程编程中。
work_queue = queue.Queue()
dir_name = SAVE_DIR + "_muti_thread_pool"
create_dir(dir_name)
for i, url in enumerate(IMAGE_URLS):
work_queue.put(url)
threads = [threading.Thread(target = thread_worker, args=(work_queue, dir_name))
for _ in range(THREAD_POOL_SIZE)]
# 启动所有线程
for thread in threads:
thread.start()
# 阻塞至队列中所有的元素都被接收和处理完毕
work_queue.join()
while threads:
threads.pop().join()
def process_worker(url):
save_path = os.path.join(SAVE_DIR + "_process_pool", f"image{url.split('/')[-3]}.jpg")
download_image(url, save_path)
@timer
def process_pool_download():
create_dir(SAVE_DIR + "_process_pool")
# Pool 类创建一个进程池,大小默认为 CPU 的核数。
with multiprocessing.Pool() as pool:
# 自动将一个可迭代对象(如列表、元组等)中的所有元素分配给多个进程或线程,
# 以进行并行计算,然后返回结果列表。它会使进程阻塞直到结果返回。
_ = pool.map(process_worker, IMAGE_URLS)
if __name__ == "__main__":
single_thread_download()
multi_thread_download()
thread_pool_download()
process_pool_download()
================================================
FILE: 1-math_ml_basic/src/python/python_basic.py
================================================
# 优点1: 生成器可以用于生成无限序列,而列表生成式只能用于有限序列。
def fibonacci():
prev, curr = 0, 1
while True:
yield curr
prev, curr = curr, prev + curr
for i, fib in enumerate(fibonacci()):
print("fib(%d) = %d" % (i, fib))
if i > 10:
break
# 优点2:处理大型数据集时,生成器可以一次生成一个元素,而不是在内存中创建整个列表,
# 这可以大大减少内存使用。下面是一个使用生成器来读取大型文本文件的伪代码:
"""
def read_large_file(file_path):
with open(file_path) as f:
while True:
data = f.readline() # 每次读取文件下一行内容
if not data:
break
yield data
for line in read_large_file("large_file.txt"):
print(line)
# process_data(line)
"""
import time
def timer(func):
"""decorator: print the cost time of run function"""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')
return result
return wrapper
def runTime(func):
"""decorator: print the cost time of run function"""
def wapper(arg, *args, **kwargs):
start = time.time()
res = func(arg, *args, **kwargs)
end = time.time()
print("="*80)
print("function name: %s" %func.__name__)
print("run time: %.4fs" %(end - start))
print("="*80)
return res
return wapper
@timer
def fib(n):
result_list = []
prev, curr = 0, 1
while n > 0:
result_list.append(curr)
prev, curr = curr, prev + curr
n -= 1
return result_list
result = fib(300)
# print(result)
import functools
def cache(func):
cached_results = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cached_results:
return cached_results[key]
else:
result = func(*args, **kwargs)
cached_results[key] = result
return result
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
print(fibonacci(20))
def my_decorator(func):
def wrapper(*args, **kwargs):
# 在这里可以访问原函数的参数
print("Function arguments:", args[0], args[1])
result = func(*args, **kwargs)
return result
return wrapper
@my_decorator
def example_function(x, y):
return x + y
example_function(2, 3)
##################### 1,面向对象基础编程-继承和多态 #########################
import math
class Shape:
def area(self):
pass
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.__name__ = 'Rectangle'
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.__name__ = 'Circle'
self.radius = radius
def area(self):
return math.pi * self.radius * self.radius
def perimeter(self):
return 2 * math.pi * self.radius
def calculate(shape):
print("The class name is type", shape.__name__)
print(f"Area: {shape.area():.2f}", shape.area())
print(f"Perimeter: {shape.perimeter():.2f}", shape.perimeter())
rectangle = Rectangle(5, 10)
circle = Circle(5)
shapes = [rectangle, circle]
for shape in shapes:
calculate(shape)
"""
The class name is type Rectangle
Area: 50.00 50
Perimeter: 30.00 30
The class name is type Circle
Area: 78.54 78.53981633974483
"""
##################### 2,面向对象基础编程-@property #########################
class Person:
def __init__(self, age):
self._age = age
self._age_group = self._calculate_age_group()
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative")
elif value > 150:
raise ValueError("Age is too large")
else:
self._age = value
self._age_group = self._calculate_age_group()
@property
def age_group(self):
return self._age_group
def _calculate_age_group(self):
if self._age < 18:
return "Age range: under 18"
elif self._age < 65:
return "Age range: 18-64"
else:
return "Age range: 65 or over"
person = Person(30)
print(person.age,",", person.age_group) # Output: 30, Age range: 18-64
#@property 和 @age.setter 装饰器使得 age 属性看起来像一个普通的属性,但实际上它是一个方法。
person.age = 70
print(person.age,",",person.age_group) # Output: 70,Age range: 65 or over
# person.age = -10 # Raises ValueError
##################### 3,面向对象基础编程-多重继承 #########################
class A:
def method1(self):
print("A method1")
class B:
def method1(self):
print("B method1")
def method2(self):
print("B method2")
class C(A, B):
def method3(self):
# super() 函数会自动查找方法调用的下一个继承类,并调用该类中的同名方法。
super().method1()
super().method2()
print("C method3")
c = C()
c.method3()
"""
A method1
B method2
C method3
"""
##################### 4,面向对象高级编程-定制类 #########################
class IntList:
def __init__(self, data):
self._data = data
def __len__(self):
return len(self._data)
def __getitem__(self, index):
if isinstance(index, int):
return self._data[index]
elif isinstance(index, slice):
return IntList(self._data[index])
def __setitem__(self, index, value):
if isinstance(index, int) and isinstance(value, int):
self._data[index] = value
elif isinstance(index, slice) and isinstance(value, IntList):
self._data[index] = value._data
def __delitem__(self, index):
if isinstance(index, int):
del self._data[index]
elif isinstance(index, slice):
del self._data[index]
def __contains__(self, item):
return item in self._data
def __iter__(self):
return iter(self._data)
def __repr__(self):
return str(self._data)
int_list = IntList([1, 2, 3, 4, 5])
print(len(int_list)) # 输出 5
print(int_list[0]) # 输出 1
print(int_list[1:4]) # 输出 [2, 3, 4]
int_list[0] = 10
print(int_list) # 输出 [10, 2, 3, 4, 5]
int_list[1:4] = IntList([20, 30])
print(int_list) # 输出 [10, 20, 30, 5]
del int_list[1]
print(int_list) # 输出 [10, 30, 5]
print(30 in int_list) # 输出 True
for item in int_list:
print(item) # 依次输出 10, 30, 5
##################### 5,面向对象高级编程-元类 #########################
import datetime
class Meta(type):
def __init__(cls, name, bases, attrs):
cls.created_at = datetime.datetime.now()
super().__init__(name, bases, attrs)
class MyClass(metaclass=Meta):
@property
def name(self):
return "MyClass class"
# 输出 MyClass class create at time: 2023-03-07 00:39:38.628766
print(MyClass().name, "create at time:", MyClass.created_at)
##################### 6,面向对象编程-实例方法、类方法和静态方法 #########################
class StringUtils:
@staticmethod
def reverse_string(string):
"""用于反转给定的字符串"""
return string[::-1]
@classmethod
def count_characters(cls, string):
"""用于计算给定字符串的字符数"""
return len(string)
def __init__(self, string):
self.string = string
def reverse_instance_string(self):
"""用于反转字符串对象中的字符串"""
return self.string[::-1]
# 使用工具类
print(StringUtils.reverse_string("Hello, world!"))
print(StringUtils.count_characters("Hello, world!"))
s = StringUtils("Hello, world!")
print(s.reverse_instance_string())
"""
!dlrow ,olleH
13
!dlrow ,olleH
"""
#################################7,dataclass 作用#################################
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
profession: str
# 创建数据类的实例
person = Person("John Doe", 30, "Engineer")
# 打印数据类的实例
print(person)
from functools import total_ordering
#################################7,functools#################################
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, age):
self.age = age
def __lt__(self, other):
if isinstance(other, Student):
return self.age < other.age
else:
raise AttributeError("Incorrect attribute!")
def __eq__(self, other):
if isinstance(other, Student):
return self.age == other.age
else:
raise AttributeError("Incorrect attribute!")
liming = Student(20)
lihua = Student(30)
print(liming < lihua)
print(liming <= lihua)
print(liming > lihua)
print(liming >= lihua)
print(liming == lihua)
def get_dict_depth(d, depth=0):
if not isinstance(d, dict):
return depth
if not d:
return depth
return max(get_dict_depth(v, depth + 1) for v in d.values())
# 测试字典
my_dict = {
'a': 1,
'b': {
'c': 2,
'd': {
'e': 3
}
}
}
depth = get_dict_depth(my_dict)
print("字典的深度为:", depth)
================================================
FILE: 1-math_ml_basic/src/python/thop_demo.py
================================================
import torch
from torchvision.models import resnet50
from thop import profile
model = resnet50()
input = torch.randn(1, 3, 224, 224)
macs, params = profile(model, inputs=(input, ))
print("flops: %.2f\nparameters: %.2f" % (macs, params))
================================================
FILE: 1-math_ml_basic/ssh远程登录服务.md
================================================
`SSH`(安全外壳协议 Secure Shell Protocol,简称SSH)是一种加密的网络传输协议,用于在网络中实现客户端和服务端的连接,典型的如我们在本地电脑通过 `SSH`连接远程服务器,从而做开发,Windows、macOS、Linux都有自带的 `SSH` 客户端,但是在Windows上使用 `SSH` 客户端的体验并不是很好,所以我们一般使用 `Xshell` 来代替。
## 一 准备工作
### 1.1 安装 SSH 客户端
为了建立 SSH 远程连接,需要两个组件:客户端和相应服务端组件,SSH 客户端是我们安装在本地电脑的软件;而服务端,也需有一个称为 SSH 守护程序的组件,它不断地侦听特定的 TCP/IP 端口以获取可能的客户端连接请求。 一旦客户端发起连接,SSH 守护进程将以软件和它支持的协议版本作为响应,两者将交换它们的标识数据。如果提供的凭据正确,SSH 会为适当的环境创建一个新会话。
MacOS 系统自带 SSH 客户端,可以直接使用,Windows 系统需要安装 `Xshell` 客户端软件,大部分 Linux 发行版系统都自带 SSH 客户端,可以直接使用,可通过 `ssh -V` 命令查看当前系统是否有 SSH 客户端。
```bash
[root@VM-0-2-centos ~]# ssh -V
OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017
```
### 1.2 安装 SSH 服务端
Linux 系统检查 ssh 服务端是否可用的命令有好几种,比如直接看是否有 `ssh` 进程在运行:
```bash
ps -ef | grep ssh
```
运行以上后,输出结果示例如下,有 sshd 进程在运行,说明 ssh 服务端可用。
```bash
-bash-4.3$ ps -e|grep ssh
336 ? 00:00:00 sshd
358 ? 00:00:00 sshd
1202 ? 00:00:00 sshd
1978 ? 00:00:00 sshd
1980 ? 00:00:00 sshd
2710 ? 00:00:00 sshd
2744 ? 00:00:00 sshd
2829 ? 00:00:00 sshd
2831 ? 00:00:00 sshd
9864 ? 00:00:00 sshd
9893 ? 00:00:02 sshd
```
对于 Ubuntu 系统,可通过以下命令检查 `OpenSSH` 服务端软件是否可用:
```bash
ssh localhost # 不同 Linux 系统输出可能不一样
```
## 二 基于密码的登录连接
典型用法,只需输入以下命令即可连接远程服务器。
```bash
# ssh连接默认端口是22,如果本地机用户名和远程机用户名一致,可以省略用户名
ssh username@host_ip
# 也可以指定连接端口
ssh -p port user@host_ip
```
上述命令是典型的 SSH 连接远程服务器的命令,如果是第一次连接运行后会得到以下提示,正常输入 `yes`,然后输入账号密码即可连接成功:
```bash
The authenticity of host '81.69.58.141 (81.69.58.141)' can't be established.
ED25519 key fingerprint is SHA256:QW5nscbIadeqedp7ByOSUF+Z45rxWGYJvAs3TTmTb0M.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Last login: Tue Feb 28 15:33:06 2023 from xx.xx.xx.xx
```
## 三 基于公钥登录连接
前面的命令是通过密码(私钥)登录,这样比较麻烦,因为每次登录我们都需要**输入密码**,因此我们可以选择 SSH 的公钥登录连接方式,省去输入密码的步骤。
公钥登录的原理,是先在本地机器上生成一对**公钥和私钥**,然后手动把公钥上传到远程服务器。这样每次登录时,远程主机会向用户发送一段随机字符串,而用户会用自己的私钥对这段随机字符串进行加密,然后把加密后的字符串发送给远程主机,远程主机会用用户的公钥对这段字符串进行解密,如果解密后的字符串和远程主机发送的随机字符串一致,那么就认为用户是合法的,允许登录。
只需要把私钥传给远程服务器,远程服务器就可以验证私钥是否是对应的公钥,如果是就允许登录,这样就不需要输入密码了。
SSH 支持多种用于身份验证密钥的公钥算法, 包括 RSA、DSA、ECDSA 和 ED25519 等,其中 RSA 算法是最常用的,因为它是 SSH 协议的默认算法,所以我们这里以 `RSA` 算法为例来生成密钥,并配置免密码远程连接。
`ssh-keygen` 是为 SSH 创建新的身份验证密钥对的工具。此类密钥对用于自动登录、单点登录和验证主机,常用参数定义如下:
- `-t` 参数指定密钥类型
- `-b` 参数指定密钥长度
### 3.1 多行命令配置
**基于公钥登录连接(多个命令)的具体步骤如下**:
**本地终端**运行 `ssh-keygen -t rsa -b 4096` 命令生成密钥对,运行后会提示输入密钥保存路径,直接回车即可,保存在默认路径下,然后会提示输入密钥密码,这里我们不设置密码,直接回车即可,然后会提示再次输入密码,这里也不设置密码,直接回车即可,最后会提示密钥生成成功,如下图所示,可以看出 `~/.ssh/` 目录下,会新生成两个文件:`id_rsa.pub` 和 `id_rsa`,分别是公钥和私钥文件。

密钥创建完成后,可手动将本地 `.ssh` 目录下的 `id_rsa.pub` 文件内容添加到目标服务器的 `~/.ssh/authorized_keys` 文件中,如果目标服务器没有 `.ssh` 目录,需要先创建 `.ssh` 目录,然后再创建 `authorized_keys` 文件,然后再添加文件内容。具体操作命令如下:
```bash
# 1,本地终端运行命令
cat ~/.ssh/id_rsa.pub # 查看本地公钥文件内容,并复制
# 2,远程终端运行命令,有 authorized_keys 文件则跳过
mkdir -p ~/.ssh # 创建 .ssh 目录
touch ~/.ssh/authorized_keys # 创建 authorized_keys 文件
# 3,然后将本地公钥文件内容粘贴到 `authorized_keys` 文件中,保存退出
```
上述步骤展开讲是为了让大家理解步骤,实际执行过程,可通过下述 2 条命令自动完成**ssh 基于公钥免输入密码连接远程服务器**:
```bash
ssh-keygen -t rsa -b 4096 # 生成公钥
ssh-copy-id -i ~/.ssh/id_rsa.pub user_name@host_ip # 运行后,它会要求你输入一次服务器密码。
```
如果上述步骤执行完成后,依然要输入密码完成 ssh 连接,可重点检查目录权限问题,以及不要手动复制公钥文件。
```bash
# 1. 确保你的家目录只有用户自己能写入
chmod 755 ~
# 2. 确保 .ssh 目录只有用户自己能进
chmod 700 ~/.ssh
# 3. 远程主机的的公钥文件只有用户自己能读写
chmod 600 /home/honggao/.ssh/authorized_keys
```
<center>
<img src="../images/ssh/ssh_dir_perm.jpg" width="80%" alt="ssh_dir_perm">
</center>
### 3.2 一行命令配置
**基于公钥登录连接,也可通过一键完成公钥登录连接的配置命令如下**:
```bash
$ ssh username@host "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" < ~/.ssh/id_rsa.pub
```
只要将公钥文件内容写入远程服务器的 `authorized_keys` 的文件,公钥登录的设置就完成了,后续远程连接就不用每次输入密码了!
`Github` 提交代码的时候,也是通过公钥登录连接的方式,只要将本地的公钥文件内容添加到 github 的 `authorized_keys` 文件中,就可以免密码提交代码了,原理是一模一样的。
## 四 VSCode 远程连接
VSCode 也支持远程连接,可以通过 `Remote-SSH` 插件来实现,具体操作步骤如下:
1,在 VSCode 中安装 [Remote-SSH 插件](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh)。
2,windows 系统 `ctrel + shift + p` 命令打开命令面板,输入 `Remote-SSH: Connect to Host...`,然后选择 `SSH Configuration`,或者通过左侧菜单栏的 `Remote Explorer` -> `SSH Targets` -> `SSH Configuration` 进入。如下图所示:

3,然后会打开 `~/.ssh/config` 配置文件,可以参考如下所示模板进行配置:
```bash
# Read more about SSH config files: https://linux.die.net/man/5/ssh_config
Host JumpMachine
HostName xxx.xxx.xxx.xxx
# 你跳板机的用户名
User username
Host T4
# 目标机的ip地址
HostName xxx.xxx.xxx.xxx
# 你目标机的用户名
User username
# 目标机登录端口
Port 22
IdentityFile ~/.ssh/id_rsa
# macos系统: ProxyCommand ssh -q -W %h:%p JumpMachine
ProxyCommand ssh -q -W %h:%p JumpMachine
```
4,本地机生产公钥并追加到远程服务器 `authorized_keys` 中的步骤,参考第三章。
5,配置完成后,保存退出,然后在 VSCode 中,点击左侧菜单栏的 `Remote Explorer` -> `SSH Targets` -> `T4`,即可连接到远程服务器。
## 参考资料
1. [维基百科-Secure Shell](https://zh.wikipedia.org/zh-hans/Secure_Shell)
2. [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen#what-is-ssh-keygen?)
3. [SSH原理与运用(一):远程登录](https://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html)
================================================
FILE: 1-math_ml_basic/transformers库快速入门.md
================================================
- [一,Transformers 术语](#一transformers-术语)
- [1.1,token、tokenization 和 tokenizer](#11tokentokenization-和-tokenizer)
- [1.2,input IDs](#12input-ids)
- [1.3,attention mask](#13attention-mask)
- [1.4,特殊 tokens 的意义](#14特殊-tokens-的意义)
- [1.5,decoder models](#15decoder-models)
- [1.6,架构与参数](#16架构与参数)
- [二,Transformers 功能](#二transformers-功能)
- [API 概述](#api-概述)
- [三,快速上手](#三快速上手)
- [3.1,transformer 模型类别](#31transformer-模型类别)
- [3.2,Pipeline](#32pipeline)
- [3.3,AutoClass](#33autoclass)
- [3.3.1,AutoTokenizer](#331autotokenizer)
- [3.3.2,AutoModel](#332automodel)
- [参考链接](#参考链接)
## 一,Transformers 术语
### 1.1,token、tokenization 和 tokenizer
`token`: 可以理解为最小语义单元,翻译的话可以是词元、令牌、词,也可以是 word/char/subword,单理解就是单词和标点。
`tokenization`: 是指**分词**过程,目的是将输入序列划分成一个个词元(`token`),保证各个词元拥有相对完整和独立的语义,以供后续任务(比如学习 embedding 或作为 LLM 的输入)使用。
`Tokenizer`: 在 transformers 库中,`tokenizer` 就是实现 `tokenization` 的对象,每个 tokenizer 会有不同的 vocabulary。在代码中,tokenizer 用以将输入文本序列划分成 tokenizer vocabulary 中可用的 `tokens`。
举两个 tokenization 例子:
- “VRAM” 通常不在词汇表中,所以其通常会被划分成 “V”, “RA” and “M” 这样的 `tokens`。
- 我是中国人->['我', '是', '中国人']
### 1.2,input IDs
`LLM` 唯一必须的输入是 `input ids`,本质是 `tokens` 索引(Indices of input sequence tokens in the vocabulary.),即数整数向量。
- 将输入文本序列转换成 tokens,即 tokenized 过程;
- 将输入文本序列转换成 input ids,即输入编码过程,数值对应的是 tokenizer 词汇表中的索引,
Transformer 库实现了不同模型的 tokenizer。下面代码展示了将输入序列转换成 tokens 和 input_ids 的结果。
```python
from transformers import BertTokenizer
sequence = "A Titan RTX has 24GB of VRAM"
tokenizer = BertTokenizer.from_pretrained("bert-base-multilingual-cased")
tokenized_sequence = tokenizer.tokenize(sequence) # 将输入序列转换成tokens,tokenized 过程
inputs = tokenizer(sequence) # 将输入序列转化成符合模型输入要求的 input_ids,编码过程
encoded_sequence = inputs["input_ids"]
print(tokenized_sequence)
print(encoded_sequence)
print("[INFO]: length of tokenized_sequence and encoded_sequence:", len(tokenized_sequence), len(encoded_sequence))
"""
['A', 'Titan', 'RT', '##X', 'has', '24', '##GB', 'of', 'VR', '##AM']
[101, 138, 28318, 56898, 12674, 10393, 10233, 32469, 10108, 74727, 36535, 102]
[INFO]: length of tokenized_sequence and encoded_sequence: 10 12
"""
```
值得注意的是,**调用 `tokenizer()` 函数返回的是字典对象**,包含相应模型正常工作所需的所有参数,tokens 索引在键 `input_ids` 对应的键值中。同时,**tokenizer 会自动填充 "special tokens"**(如果相关模型依赖它们),这也是 tokenized_sequence 和 encoded_sequence 列表中长度不一致的原因。
```python
decoded_sequence = tokenizer.decode(encoded_sequence)
print(decoded_sequence)
"""
[CLS] A Titan RTX has 24GB of VRAM [SEP]
"""
```
### 1.3,attention mask
注意掩码(`attention mask`)是一个可选参数,一般在将输入序列进行**批处理**时使用。作用是告诉我们哪些 `tokens` 应该被关注,哪些不用。因为如果输入的序列是一个列表,每个序列长度是不一样的,通常是通过填充的方式把他们处理成同一长度。原始 token id 是我们需要关注的,填充的 id 是不用关注的。
attention mask 是二进制张量类型,值为 `1` 的位置索引对应的原始 `token` 表示应该注意的值,而 `0` 表示填充值。
示例代码如下:
```python
from transformers import AutoTokenizer
sentence_list = ["We are very happy to show you the 🤗 Transformers library.",
"Deepspeed is faster"]
model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
tokenizer = AutoTokenizer.from_pretrained(model_name)
padded_sequences = tokenizer(sentence_list, padding=True, return_tensors="pt") # 字典类型
print(padded_sequences["input_ids"])
print(padded_sequences["attention_mask"])
"""
tensor([[101, 11312, 10320, 12495, 19308, 10114, 11391, 10855, 10103, 100, 58263, 13299, 119, 102],
[101, 15526, 65998, 54436, 10127, 51524, 102, 0, 0, 0, 0, 0, 0, 0]])
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]])
"""
```

### 1.4,特殊 tokens 的意义
我们在模型的 checkpoints 目录下的配置文件中,经常能看到 eop_token、pad_token、bos_token、eos_token 这些与文本序列处理相关的特殊 `token`,它们代表的意义如下:
1. `bos_token`( Beginning of Sentence Token):序列开始标记,它表示文本序列的起始位置。
2. `eos_token`( End of Sentence Token):**序列结束标记**,它表示文本序列的结束位置。
3. `eop_token`(End of Paragraph Token)段落的结束标志,是用于表示段落结束的特殊标记。
4. `pad_token`(Padding Token):填充标记,它用于将文本序列填充到相同长度时使用的特殊 token。
### 1.5,decoder models
decoder 模型也称为自回归(auto-regressive)模型、causal language models,其按顺序阅读输入文本并必须预测下一个单词,在训练中会阅读**添加掩码的句子**。
### 1.6,架构与参数
- **架构**:模型的骨架,包含每个层的类别及定义、各个层的连接方式等等内容。
- **Checkpoints**:给定架构中会被加载的权重。
- **模型**:一个笼统的术语,没有“架构”或“参数”那么精确:它可以指两者。
## 二,Transformers 功能
[Transformers](https://github.com/huggingface/transformers) 库提供创建 transformer 模型和加载使用共享模型的功能;另外,[模型中心(hub)](https://huggingface.co/models)包含数千个可以任意下载和使用的预训练模型,也支持用户上传模型到 Hub。
### API 概述
Transformers 库的 `API` 主要包括以下三种:
1. **MAIN CLASSES**:主要包括配置(configuration)、模型(model)、分词器(tokenizer)和流水线(pipeline)这几个最重要的类。
2. **MODELS**:库中和每个模型实现有关的类和函数。
3. **INTERNAL HELPERS**:内部使用的工具类和函数。
## 三,快速上手
### 3.1,transformer 模型类别
Transformer 模型架构主要由两个部件组成:
- **Encoder (左侧)**: 编码器接收输入并构建其表示(其特征)。这意味着对模型进行了优化,以从输入中获得理解。
- **Decoder (右侧)**: 解码器使用编码器的表示(特征)以及其他输入来生成目标序列。这意味着该模型已针对生成输出进行了优化。

上述两个部件中的每一个都可以作为模型架构独立使用,具体取决于任务:
- **Encoder-only models**: 也叫自动编码 Transformer 模型,如 BERT-like 系列模型,适用于需要理解输入的任务。如句子分类和命名实体识别。
- **Decoder-only models**: 也叫自回归 Transformer 模型,如 GPT-like 系列模型。适用于生成任务,如**文本生成**。
- **Encoder-decoder models** 或者 **sequence-to-sequence models**: 也被称作序列到序列的 Transformer 模型,如 BART/T5-like 系列模型。适用于需要根据输入进行生成的任务,如翻译或摘要。
下表总结了目前的 transformers 架构模型类别、示例以及适用任务:
| 模型 | 示例 | 任务 |
| ------------- | ------------------------------------------ | ---------------------------------------- |
| 编码器 | ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa | 句子分类、命名实体识别、从文本中提取答案 |
| 解码器 | CTRL, GPT, GPT-2, Transformer XL | 文本生成 |
| 编码器-解码器 | BART, T5, Marian, mBART | 文本摘要、翻译、生成问题的回答 |
### 3.2,Pipeline
Transformers 库支持通过 pipeline() 函数设置 `task` 任务类型参数,来跑通不同模型的推理,可实现一行代码跑通跨不同模态的多种任务,其支持的任务列表如下:
| **任务** | **描述** | **模态** | **Pipeline** |
| ------------ | -------------------------------------------------------- | --------------- | --------------------------------------------- |
| 文本分类 | 为给定的文本序列分配一个标签 | NLP | pipeline(task="sentiment-analysis") |
| 文本生成 | 根据给定的提示生成文本 | NLP | pipeline(task="text-generation") |
| 命名实体识别 | 为序列里的每个token分配一个标签(人, 组织, 地址等等) | NLP | pipeline(task="ner") |
| 问答系统 | 通过给定的上下文和问题, 在文本中提取答案 | NLP | pipeline(task="question-answering") |
| 掩盖填充 | 预测出正确的在序列中被掩盖的token | NLP | pipeline(task="fill-mask") |
| 文本摘要 | 为文本序列或文档生成总结 | NLP | pipeline(task="summarization") |
| 文本翻译 | 将文本从一种语言翻译为另一种语言 | NLP | pipeline(task="translation") |
| 图像分类 | 为图像分配一个标签 | Computer vision | pipeline(task="image-classification") |
| 图像分割 | 为图像中每个独立的像素分配标签(支持语义、全景和实例分割) | Computer vision | pipeline(task="image-segmentation") |
| 目标检测 | 预测图像中目标对象的边界框和类别 | Computer vision | pipeline(task="object-detection") |
| 音频分类 | 给音频文件分配一个标签 | Audio | pipeline(task="audio-classification") |
| 自动语音识别 | 将音频文件中的语音提取为文本 | Audio | pipeline(task="automatic-speech-recognition") |
| 视觉问答 | 给定一个图像和一个问题,正确地回答有关图像的问题 | Multimodal | pipeline(task="vqa") |

以下代码是通过 pipeline 函数实现对文本的情绪分类。
```python
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
print(classifier("I've been waiting for a HuggingFace course my whole life."))
# [{'label': 'POSITIVE', 'score': 0.9598049521446228}]
```
在 `NLP` 问题中,除了使用 `pipeline()` 任务中默认的模型,也可以通过指定 `model` 和 `tokenizer` 参数来自动查找相关模型。
### 3.3,AutoClass
Pipeline() 函数背后实际是通过 “AutoClass” 类,实现**通过预训练模型的名称或路径自动查找其架构**的快捷方式。通过为任务选择合适的 `AutoClass` 和它关联的预处理类,来重现使用 `pipeline()` 的结果。
#### 3.3.1,AutoTokenizer
分词器(`tokenizer`)的作用是负责预处理文本,将输入文本(input prompt)转换为**数字数组**(array of numbers)来作为模型的输入。`tokenization` 过程主要的规则包括:如何拆分单词和什么样级别的单词应该被拆分。值得注意的是,实例化 tokenizer 和 model 必须是同一个模型名称或者 `checkpoints` 路径。
对于 `LLM` ,通常还是使用 `AutoModel` 和 `AutoTokenizer` 来加载预训练模型和它关联的分词器。
```py
from transformers import AutoModel, AutoTokenizer
tokenizer = LlamaTokenizer.from_pretrained(model_name_or_path)
model = AutoModel.from_pretrained(model_name_or_path, torch_dtype=torch.float16)
```
一般使用 `AutoTokenizer` 加载分词器(`tokenizer`):
```python
from transformers import AutoTokenizer
model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
tokenizer = AutoTokenizer.from_pretrained(model_name)
encoding = tokenizer("We are very happy to show you the 🤗 Transformers library.")
print(encoding)
"""
{'input_ids': [101, 11312, 10320, 12495, 19308, 10114, 11391, 10855, 10103, 100, 58263, 13299, 119, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
"""
```
`tokenizer` 的返回是包含了如下“键”的字典:
- [input_ids](https://huggingface.co/docs/transformers/v4.29.1/zh/glossary#input-ids): 用数字表示的 `token`。
- [attention_mask](https://huggingface.co/docs/transformers/v4.29.1/zh/.glossary#attention-mask): 应该关注哪些 `token` 的指示。
tokenizer() 函数还**支持列表作为输入,并可填充和截断文本, 返回具有统一长度的批次**:
```python
pt_batch = tokenizer(
["We are very happy to show you the 🤗 Transformers library.", "We hope you don't hate it."],
padding=True,
truncation=True,
max_length=512,
return_tensors="pt",
)
```
### 3.3.2,AutoModel
Transformers 提供了一种简单统一的方式来加载预训练的模型实例,即可以像加载 `AutoTokenizer` 一样加载 `AutoModel`,我们所需要提供的必须参数只有模型名称或者 `checkpoints` 路径,即只需输入初始化的 checkpoint(检查点)或者模型名称就可以返回正确的模型体系结构。示例代码如下所示:
```python
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from torch import nn
model_name = "nlptown/bert-base-multilingual-uncased-sentiment"
tokenizer = AutoTokenizer.from_pretrained(model_name) # 会下载 vocab.txt 词表
# ["We are very happy to show you the 🤗 Transformers library.", "We hope you don't hate it."],
pt_batch = tokenizer(
"We are very happy to show you the 🤗 Transformers library.", "We hope you don't hate it.",
padding=True,
truncation=True,
max_length=512,
return_tensors="pt",
)
pt_model = AutoModelForSequenceClassification.from_pretrained(model_name) # 会下载 pytorch_model.bin 模型权重
pt_outputs = pt_model(**pt_batch) # ** 可解包 pt_batch 字典
pt_predictions = nn.functional.softmax(pt_outputs.logits, dim=-1) # 在 logits上应用softmax函数来查询概率
print(pt_predictions)
print(pt_model.config.id2label) # {0: '1 star', 1: '2 stars', 2: '3 stars', 3: '4 stars', 4: '5 stars'}
```
> 注意,Transformers 模型默认情况下是需要多个句子。虽然这里输入看起来是一个句子,但实际 `tokenizer` 不仅将 input ids 列表转换为张量,还在其顶部添加了一个维度(`batch` 维度)。
程序运行结果输出如下所示。
> tensor([[0.0021, 0.0018, 0.0115, 0.2121, 0.7725],
> [0.2084, 0.1826, 0.1969, 0.1755, 0.2365]], grad_fn=<SoftmaxBackward0>)
## 参考链接
1. [HuggingFace Transformers 官方文档](https://huggingface.co/docs/transformers/v4.29.1/zh/quicktour)
2. [NLP Course](https://huggingface.co/learn/nlp-course/zh-CN/chapter1/1)
3. [NLP领域中的token和tokenization到底指的是什么](https://www.zhihu.com/question/64984731)
================================================
FILE: 1-math_ml_basic/深度学习基础-机器学习基本原理.md
================================================
---
layout: post
title: 深度学习基础-机器学习基本原理
date: 2022-11-04 23:00:00
summary: 深度学习是机器学习的一个特定分支。我们要想充分理解深度学习,必须对机器学习的基本原理有深刻的理解。大部分机器学习算法都有超参数(必须在学习算法外手动设定)。机器学习本质上属于应用统计学,其更加强调使用计算机对复杂函数进行统计估计。
categories: DeepLearning
---
- [前言](#前言)
- [5.1 学习算法](#51-学习算法)
- [5.1.1 任务 $T$](#511-任务-t)
- [5.1.2 性能度量 $P$](#512-性能度量-p)
- [5.1.3 经验 $E$](#513-经验-e)
- [5.1.4 示例: 线性回归](#514-示例-线性回归)
- [5.2 容量、过拟合和欠拟合](#52-容量过拟合和欠拟合)
- [5.2.1 没有免费午餐定理](#521-没有免费午餐定理)
- [5.2.2 正则化](#522-正则化)
- [5.3 超参数和验证集](#53-超参数和验证集)
- [5.3.1 验证集的作用](#531-验证集的作用)
- [5.3.2 交叉验证](#532-交叉验证)
- [5.4 估计、偏差和方差](#54-估计偏差和方差)
- [5.4.1 点估计](#541-点估计)
- [5.4.2 偏差](#542-偏差)
- [5.4.3 方差和标准差](#543-方差和标准差)
- [5.4.4 权衡偏差和方差以最小化均方误差](#544-权衡偏差和方差以最小化均方误差)
- [5.5 最大似然估计](#55-最大似然估计)
- [5.6 贝叶斯统计](#56-贝叶斯统计)
- [5.7 监督学习算法](#57-监督学习算法)
- [5.8 无监督学习算法](#58-无监督学习算法)
- [5.8.1 PCA 降维](#581-pca-降维)
- [5.8.2 k-均值聚类](#582-k-均值聚类)
- [5.9 随机梯度下降](#59-随机梯度下降)
- [5.10 构建机器学习算法 pipeline](#510-构建机器学习算法-pipeline)
- [参考资料](#参考资料)
> 本文大部分内容参考《深度学习》书籍,从中抽取重要的知识点,并对部分概念和原理加以自己的总结,适合当作原书的补充资料阅读,也可当作快速阅览机器学习原理基础知识的参考资料。
## 前言
**深度学习是机器学习的一个特定分支**。我们要想充分理解深度学习,必须对机器学习的基本原理有深刻的理解。
大部分机器学习算法都有**超参数**(必须在学习算法外**手动设定**)。**机器学习本质上属于应用统计学**,其更加强调使用计算机对复杂函数进行**统计估计**,而较少强调围绕这些函数证明置信区间;因此我们会探讨两种统计学的主要方法: **频率派估计和贝叶斯推断**。同时,大部分机器学习算法又可以分成**监督学习**和**无监督学习**两类;本文会介绍这两类算法定义,并给出每个类别中一些算法示例。
本章内容还会介绍如何组合不同的算法部分,例如优化算法、代价函数、模型和数据 集,来建立一个机器学习算法。最后,在 5.11 节中,我们描述了一些限制传统机器学习泛化能力的因素。正是这些挑战推动了克服这些障碍的深度学习算法的发展。
> 大部分深度学习算法都是基于随机梯度下降算法进行求解的。
## 5.1 学习算法
机器学习算法是一种能够从数据中学习的算法。这里所谓的“学习“是指:“如果计算机程序在任务 $T$ 中的性能(以 $P$ 衡量)随着经验 $E$ 而提高,则可以说计算机程序从经验 $E$ 中学习某类任务 $T$ 和性能度量 $P$。”-来自 `Mitchell` (`1997`)
> 经验 $E$,任务 $T$ 和性能度量 $P$ 的定义范围非常宽广,本文不做详细解释。
### 5.1.1 任务 $T$
从 “任务” 的相对正式的定义上说,学习过程本身不能算是任务。学习是我们所谓的获取完成任务的能力。机器学习可以解决很多类型的任务,一些非常常见的机器学习任务列举如下:
1. **分类**:在这类任务中,计算机程序需要指定某些输入属于 $k$ 类中的哪一类,例如图像分类中的二分类问题,多分类、单标签问题、多分类多标签问题。
2. **回归**:在这类任务中,计算机程序需要对给定输入预测数值。为了解决这个任务,学习算法需要输出函数 $f : \mathbb{R}^n \to \mathbb{R}$。除了返回结果的形式不一样外,这类 问题和分类问题是很像的。
3. **机器翻译**
4. **结构化输出**
5. **异常检测**
6. **合成和采样**
7. **去噪**
8. **密度估计或概率质量函数估计**
9. **输入缺失分类**
10. **转录**
11. **缺失值填补**
### 5.1.2 性能度量 $P$
为了评估机器学习算法的能力,我们必须设计其性能的**定量度量**,即算法的精度指标。通常,性能度量 $P$ 特定于系统正在执行的任务 $T$。
> 可以理解为不同的任务有不同的性能度量。
对于诸如分类、缺失输入分类和转录任务,我们通常度量模型的**准确率**(`accu- racy`)。准确率是指该模型输出正确结果的样本比率。我们也可以通过**错误率**(`error rate`)得到相同的信息。错误率是指该模型输出错误结果的样本比率。
我们使用测试集(`test set`)数据来评估系统性能,将其与训练机器学习系统的训练集数据分开。
值得注意的是,性能度量的选择或许看上去简单且客观,但是选择一个与系统理想表现能对应上的性能度量通常是很难的。
### 5.1.3 经验 $E$
根据学习过程中的不同经验,机器学习算法可以大致分类为两类
- **无监督**(`unsuper-vised`)算法
- **监督**(`supervised`)算法
**无监督学习算法**(`unsupervised learning algorithm`)训练含有很多特征的数据集,然后学习出这个数据集上有用的结构性质。在深度学习中,我们通常要学习生成数据集的整个概率分布,显式地,比如密度估计,或是隐式地,比如合成或去噪。 还有一些其他类型的无监督学习任务,例如聚类,将数据集分成相似样本的集合。
**监督学习算法**(`supervised learning algorithm`)也训练含有很多特征的数据集,但与无监督学习算法不同的是**数据集中的样本都有一个标签**(`label`)或目标(`target`)。例如,`Iris` 数据集注明了每个鸢尾花卉样本属于什么品种。监督学习算法通过研究 `Iris` 数据集,学习如何根据测量结果将样本划分为三个不同品种。
**半监督学习算法**中,一部分样本有监督目标,另外一部分样本则没有。在多实例学习中,样本的整个集合被标记为含有或者不含有该类的样本,但是集合中单独的样本是没有标记的。
大致说来,无监督学习涉及到观察随机向量 $x$ 的好几个样本,试图显式或隐式地学习出概率分布 $p(x)$,或者是该分布一些有意思的性质; 而监督学习包含观察随机向量 $x$ 及其相关联的值或向量 $y$,然后从 $x$ 预测 $y$,通常是估计 $p(y\vert x)$。术语监督学习(`supervised learning`)源自这样一个视角,教员或者老师提供目标 $y$ 给机器学习系统,指导其应该做什么。在无监督学习中,没有教员或者老师,算法必须学会在没有指导的情况下理解数据。
无监督学习和监督学习并不是严格定义的术语。它们之间界线通常是模糊的。很多机器学习技术可以用于这两个任务。
尽管无监督学习和监督学习并非完全没有交集的正式概念,它们确实有助于粗略分类我们研究机器学习算法时遇到的问题。传统地,人们将回归、分类或者结构化输出问题称为监督学习。支持其他任务的密度估计通常被称为无监督学习。
表示数据集的常用方法是**设计矩阵**(`design matrix`)。
### 5.1.4 示例: 线性回归
我们将机器学习算法定义为,通过经验以提高计算机程序在某些任务上性能的算法。这个定义有点抽象。为了使这个定义更具体点,我们展示一个简单的机器学习示例: **线性回归**(`linear regression`)。
顾名思义,**线性回归解决回归问题**。 换句话说,目标是构建一个系统,该系统可以将向量 $x \in \mathbb{R}$ 作为输入,并预测标量 $y \in \mathbb{R}$ 作为输出。在线性回归的情况下,输出是输入的线性函数。令 $\hat{y}$ 表示模型预测值。我们定义输出为
$$\hat{y} = w^{⊤}x \tag{5.3}$$
其中 $w \in \mathbb{R}^{n}$ 是**参数**(`parameter`)向量。
参数是控制系统行为的值。在这种情况下,$w_i$ 是系数,会和特征 $x_i$ 相乘之 后全部相加起来。我们可以将 $w$ 看作是一组决定每个特征如何影响预测的权重 (weight)。
通过上述描述,我们可以定义任务 $T$ : 通过输出 $\hat{y} = w^{⊤}x$ 从 $x$ 预测 $y$。
我们使用**测试集**(`test set`)来评估模型性能如何,将输入的设计矩 阵记作 $\textit{X}$(test),回归目标向量记作 $y$(test)。
**回归任务**常用的一种模型性能度量方法是计算模型在测试集上的 **均方误差**(`mean squared error`)。如果 $\hat{y}$(`test`) 表示模型在测试集上的预测值,那么均方误差表示为:
$$MSE_{test} = \frac{1}{m} \sum_{i}(\hat{y}^{(test)}-y^{(test)})_{i}^{2} \tag{5.4}$$
直观上,当 $\hat{y}^{(test)}$ = $y^{(test)}$ 时,我们会发现误差降为 0。
图 5.1 展示了线性回归算法的使用示例。

## 5.2 容量、过拟合和欠拟合
机器学习的挑战主要在于算法如何在测试集(先前未观测的新输入数据)上表现良好,而不只是在训练集上表现良好,即训练误差和泛化误差读比较小,也可理解为算法泛化性比较好。所谓泛化性(generalized)好指的是,算法在在测试集(以前未观察到的输入)上表现良好。
机器学习算法的两个主要挑战是: **欠拟合**(`underfitting`)和**过拟合**(`overfitting`)。
- 欠拟合是指模型不能在训练集上获得足够低的误差。
- 而过拟合是指训练误差和和测试误差之间的差距太大。
通过调整模型的**容量**(`capacity`),我们可以控制模型是否偏向于过拟合或者欠拟合。通俗地讲,**模型的容量是指其拟合各种函数的能力**。容量低的模型可能很难拟合训练集,容量高的模型可能会过拟合,因为记住了不适用于测试集的训练集性质。
一种控制训练算法容量的方法是选择**假设空间**(`hypothesis space`),即**学习算法可以选择作为解决方案的函数集**。例如,线性回归算法将其输入的所有线性函数的集合作为其假设空间。我们可以推广线性回归以在其假设空间中包含多项式,而不仅仅是线性函数。这样做就增加模型的容量。
> 注意,学习算法的效果不仅很大程度上受影响于假设空间的函数数量,也取决于这些函数的具体形式。
当机器学习算法的容量适合于所执行任务的复杂度和所提供训练数据的数量时,算法效果通常会最佳。容量不足的模型不能解决复杂任务。容量高的模型能够解决复杂的任务,但是当其容量高于任务所需时,有可能会过拟合。
图 5.2 展示了上述原理的使用情况。我们比较了线性,二次和 `9` 次预测器拟合真实二次函数的效果。

提高机器学习模型泛化性的早期思想是**奥卡姆剃刀**原则,即选择“最简单”的那一个模型。
统计学习理论提供了量化模型容量的不同方法。在这些中,最有名的是 **Vapnik- Chervonenkis 维度**(Vapnik-Chervonenkis dimension, VC)。`VC` 维度量二元分类 器的容量。`VC` 维定义为该分类器能够分类的训练样本的最大数目。假设存在 $m$ 个 不同 $x$ 点的训练集,分类器可以任意地标记该 $m$ 个不同的 $x$ 点,`VC` 维被定义为 $m$ 的最大可能值。
因为可以量化模型的容量,所以使得统计学习理论可以进行量化预测。统计学习理论中最重要的结论阐述了训练误差和泛化误差之间差异的上界随着模型容量增长而增长,但随着训练样本增多而下降 (`Vapnik and Chervonenkis, 1971`; `Vapnik, 1982`; `Blumer et al., 1989`; `Vapnik, 1995`)。这些边界为机器学习算法可以有效解决问题提供了理论 验证,但是它们**很少应用于实际中的深度学习算法**。一部分原因是边界太松,另一部分原因是**很难确定深度学习算法的容量**。由于有效容量受限于优化算法的能力,所以确定深度学习模型容量的问题特别困难。而且我们对深度学习中涉及的非常普遍的**非凸优化问题**的理论了解很少。
虽然更简单的函数更可能泛化(训练误差和测试误差的差距小),但我们仍然必须选择一个足够复杂的假设来实现低训练误差。通常,随着模型容量的增加,训练误差会减小,直到它逐渐接近最小可能的误差值(假设误差度量具有最小值)。通常,**泛化误差是一个关于模型容量的 U 形曲线函数**。如下图 `5.3` 所示。

### 5.2.1 没有免费午餐定理
机器学习的**没有免费午餐定理**(`Wolpert,1996`)指出,对所有可能的数据生成分布进行平均,每个分类算法在对以前未观察到的点进行分类时具有相同的错误率。换句话说,在某种意义上,**没有任何机器学习算法普遍优于其他任何算法**。
上述这个结论听着真的让人伤感,但庆幸的是,这些结论仅在我们考虑**所有可能的数据生成分布**时才成立。如果我们对实际应用中遇到的概率分布类型做出假设,那么我们可以**设计出在这些分布上表现良好的学习算法**。
这意味着机器学习研究的目标**不是找一个通用学习算法或是绝对最好的学习算法**。反之,我们的目标是理解什么样的分布与人工智能获取经验的 “真实世界” 相关,**什么样的学习算法在我们关注的数据生成分布上效果最好**。
**总结**:没有免费午餐定理清楚地阐述了没有最优的学习算法,即暗示我们必须在特定任务上设计性能良好的机器学习算法。
### 5.2.2 正则化
所谓正则化(Regularization),是指我们通过**修改学习算法,使其降低泛化误差而非训练误差**的方法。
**正则化是一种思想(策略)**,它是机器学习领域的中心问题之一,其重要性只有优化能与其相媲美。
一般正则化一个模型,通常通过对**原始损失函数**引入**额外信息**(也叫惩罚项/正则化项),新的损失函数变成了**原始损失函数 + 惩罚项**对形式。惩罚项通常是对权重参数做一些限制,如 L1/L2 范数:
- L1: $\lambda \cdot \lVert x \rVert_{1}$,即所有元素的绝对值之和
- L2: $\lambda \cdot \lVert x \rVert_{2}$,即所有元素的绝对值平方和
如果正则化项是 $\Omega(w) = w^{\top}w$,则称为**权重衰减**(weight decay)。
在标准的随机梯度下降中,权重衰减正则化和 L2 正则化的效果相同。因此,权重衰减在一些深度学习框架中通过 L2 正则化来实现。加入了正则化项后的损失函数的变化如下:
对于均方差损失函数:
$$J(w,b)=\frac{1}{2m}\sum_{i=1}^m (z_i-y_i)^2 + \frac{\lambda}{2m}\sum_{j=1}^n{w_j^2}$$
对于交叉熵损失函数:
$$J(w,b)= -\frac{1}{m} \sum_{i=1}^m [y_i \ln a_i + (1-y_i) \ln (1-a_i)]+ \frac{\lambda}{2m}\sum_{j=1}^n{w_j^2}$$
其中 $J(w, b)$ 是最终损失函数,$w$ 是权重参数。$\lambda$ 是正则化项的超参数,需提前设置,**其控制我们对较小权重的偏好强度**。当 $\lambda = 0$,我们没有任何偏好。$\lambda$ 越大,则权重越小。最小化 $J(w)$ 会导致权重的选择在**拟合训练数据和较小权重之间进行权衡**。
> 值得注意的是。在较为复杂的优化方法(比如 Adam)中,权重衰减正则化和 L2 正则化并不等价 [Loshchilov et al., 2017b]。
**和上一节没有最优的学习算法一样,一样的,也没有最优的正则化形式**。反之,我们必须挑选一个非常适合于我们所要解决的任务的正则形式。
那么,**L2 正则化为什么能防止过拟合**呢?
我个人理解是,我们一般默认模型参数越小、模型越简单,越简单的模型越不容易过拟合。而 L2 正则化的模型拟合过程中通常都倾向于让权值尽可能小,即最后能构造出一个所有参数都比较小的模型。
有[文章](https://zhuanlan.zhihu.com/p/41631717)通过可视化加入 L1/L2 正则化后的权重分布,得出 L1/L2 正则化如实地将权重往 `0` 的方向赶,即会让权值尽可能小,但是 L1 赶得快,L2 比较慢一些,在 30 epoc h以上 L2 才有类似于 L1 的一枝独秀的分布。
## 5.3 超参数和验证集
**超参数的值不是通过学习算法本身学习出来的,而是需要算法定义者手动指定的**。
### 5.3.1 验证集的作用
通常,`80%` 的训练数据用于训练,`20%` 用于验证。验证集是用于估计训练中或训练后的泛化误差,从而**更新超参数**。
### 5.3.2 交叉验证
一个**小规模的测试集**意味着平均测试误差估计的统计不确定性,使得很难判断算法 A 是否比算法 B 在给定的任务上做得更好。解决办法是基于在原始数据上**随机采样或分离**出的不同数据集上**重复训练和测试**,最常见的就是 $k$-折交叉验证,即将数据集分成 $k$ 个 不重合的子集。测试误差可以估计为 $k$ 次计算后的平均测试误差。在第 $i$ 次测试时, 数据的第 $i$ 个子集用于测试集,其他的数据用于训练集。算法过程如下所示。
> k 折交叉验证虽然一定程度上可以解决小数据集上测试误差的不确定性问题,但代价则是增加了计算量。

## 5.4 估计、偏差和方差
统计领域为我们提供了很多工具来实现机器学习目标,不仅可以解决训练集上 的任务,还可以泛化。基本的概念,例如参数估计、偏差和方差,对于正式地刻画泛化、欠拟合和过拟合都非常有帮助。
### 5.4.1 点估计
点估计试图为一些感兴趣的量提供单个 ‘‘最优’’ 预测。一般地,感兴趣的量可以是单个参数也可以是一个向量参数,例如第 5.1.4 节线性回归中的权重,但是也有可能是整个函数。
为了区分参数估计和真实值,我们习惯将参数 $\theta$ 的点估计表示为 $\hat{\theta}$。
令 $x^{(1)}, . . . , x^{(m)}$ 是 $m$ 个独立同分布(i.i.d.)的数据点。 点估计(point esti-mator)或统计量(statistics)是这些数据的任意函数:
$$
\hat{\theta_m} =g(x^{(1)},...,x^{(m)}). \tag{5.19}
$$
### 5.4.2 偏差
估计的偏差定义如下:
$$
bias(\hat{\theta_m}) = E(\hat{\theta_m}) − \theta, \tag{5.19}
$$
其中期望作用在所有数据(看作是从随机变量采样得到的)上,$\hat{\theta}$ 是用于定义数据生成分布的 $\theta$ 的真实值。如果 $bias(\hat{\theta_m}) = 0$,那么估计量 $\hat{\theta_m}$ 则被称为是**无偏** (unbiased),同时意味着 $E(\hat{\theta_m}) = \theta$。
### 5.4.3 方差和标准差
方差记为 $Var(\hat{\theta})$ 或 $\sigma^{2}$,方差的平方根被称为标准差。
### 5.4.4 权衡偏差和方差以最小化均方误差
偏差和方差度量着估计量的两个不同误差来源。偏差度量着偏离真实函数或参数的误差期望。而方差度量着数据上任意特定采样可能导致的估计期望的偏差。
**偏差和方差的关系和机器学习容量、欠拟合和过拟合的概念紧密相联**。用 MSE 度量泛化误差(偏差和方差对于泛化误差都是有意义的)时,增加容量会增加方差,降低偏差。如图 5.6 所示,我们再次在关于容量的函数中,看到泛化误差的 U 形曲线。

## 5.5 最大似然估计
与其猜测某个函数可能是一个好的估计器,然后分析它的偏差和方差,我们更希望有一些原则,我们可以从中推导出特定的函数,这些函数是不同模型的良好估计器。最大似然估计就是其中最为常用的准则。
## 5.6 贝叶斯统计
到目前为止,我们已经描述了**频率统计**(frequentist statistics)和基于估计单一 $\theta$ 值的方法,然后基于该估计作所有的预测。 另一种方法是在进行预测时考虑所有可能的 $\theta$ 值。 后者属于**贝叶斯统计**(Bayesian statistics)的范畴。
如 5.4.1 节中讨论的,频率派的视角是真实参数 $\theta$ 是未知的定值,而点估计 $\hat{\theta}$ 是考虑数据集上函数(可以看作是随机的)的随机变量。
贝叶斯统计的视角完全不同。贝叶斯用概率反映知识状态的确定性程度。数据集能够被直接观测到,因此不是随机的。另一方面,真实参数 $\theta$ 是未知或不确定的, 因此可以表示成随机变量。
## 5.7 监督学习算法
回顾 5.1.3 节内容,简单来说,监督学习算法是给定一组输入 $x$ 和输出 $y$ 的训练 集,学习如何关联输入和输出。在许多情况下,输出 $y$ 可能难以自动收集,必须由人类“监督者”提供,但即使训练集目标是自动收集的,该术语仍然适用。
## 5.8 无监督学习算法
回顾第5.1.3节,无监督算法只处理 “特征’’,不操作监督信号。监督和无监督算法之间的区别没有规范严格的定义,因为没有客观的测试来区分一个值是特征还是监督者提供的目标。通俗地说,无监督学习的大多数尝试是指从不需要人为注释的样本的分布中提取信息。该术语通常与密度估计相关,学习从分布中采样、学习从分布中去噪、寻找数据分布的流形或是将数据中相关的样本聚类。
### 5.8.1 PCA 降维
`PCA`(Principal Component Analysis)是学习数据表示的无监督学习算法,常用于高维数据的降维,可用于提取数据的主要特征分量。
PCA 的数学推导可以从最大可分型和最近重构性两方面进行,前者的优化条件为划分后方差最大,后者的优化条件为点到划分平面距离最小。
### 5.8.2 k-均值聚类
另外一个简单的表示学习算法是 $k$-均值聚类。$k$-均值聚类算法将训练集分成 $k$ 个靠近彼此的不同样本聚类。因此我们可以认为该算法提供了 $k$-维的 one-hot 编码向量 $h$ 以表示输入 $x$。当 $x$ 属于聚类 $i$ 时,有 $h_i = 1$,$h$ 的其他项为零。
$k$-均值聚类初始化 k 个不同的中心点 ${μ^{(1)}, . . . , μ^{(k)}}$,然后迭代交换以下两个不同的步骤直到算法收敛。
1. 步骤一,每个训练样本分配到最近的中心点 $μ^{(i) }$ 所代表的聚类 $i$。
2. 步骤二,每一个中心点 $μ^{(i) }$ 更新为聚类 $i$ 中所有训练样本 $x^{(j)}$ 的均值。
关于聚类的一个问题是**聚类问题本身是病态的**。这是说没有单一的标准去度量聚类的数据在真实世界中效果如何。我们可以度量聚类的性质,例如类中元素到类中心点的欧几里得距离的均值。这使我们可以判断从聚类分配中重建训练数据的效果如何。然而我们不知道聚类的性质是否很好地对应到真实世界的性质。此外,可能有许多不同的聚类都能很好地对应到现实世界的某些属性。我们可能希望找到和 一个特征相关的聚类,但是得到了一个和任务无关的,同样是合理的不同聚类。
例如,假设我们在包含红色卡车图片、红色汽车图片、灰色卡车图片和灰色汽车图片的数据集上运行两个聚类算法。如果每个聚类算法聚两类,那么可能一个算法将汽车和卡车各聚一类,另一个根据红色和灰色各聚一类。假设我们还运行了第三个聚类算法,用来决定类别的数目。这有可能聚成了四类,红色卡车、红色汽车、灰色卡 车和灰色汽车。现在这个新的聚类至少抓住了属性的信息,但是丢失了相似性信息。 红色汽车和灰色汽车在不同的类中,正如红色汽车和灰色卡车也在不同的类中。该聚类算法没有告诉我们灰色汽车和红色汽车的相似度比灰色卡车和红色汽车的相似度更高,我们只知道它们是不同的。
## 5.9 随机梯度下降
几乎所有的深度学习算法都用到了一个非常重要的优化算法: **随机梯度下降** (stochastic gradient descent, `SGD`)。
机器学习中反复出现的一个问题是好的泛化需要大的训练集,但大的训练集的 计算代价也更大。
机器学习算法中的代价函数通常可以分解成每个样本的代价函数的总和。

随机梯度下降的核心是,**梯度是期望**,而期望可使用小规模的样本近似估计。具体来说,在算法的每一步,我们从训练集中均匀抽出一小批量(`minibatch`)样本 $B={x^{(1)},...,x^{(m′)}}$。小批量的数目 $m′$ 通常是一个相对较小的数,一般为 $2^n$(取决于显卡显卡)。重要的是,当训练集大小 m 增长时,$m′$ 通常是固定的。我们可能在拟合几十亿的样本时,但每次更新计算只用到几百个样本。

梯度下降往往被认为很慢或不可靠。以前,将梯度下降应用到非凸优化问题被认为很鲁莽或没有原则。但现在,我们知道梯度下降用于深度神经网络模型的训练时效果是不错的。优化算法不一定能保证在合理的时间内达到一个局部最小值,但它通常能及时地找到代价函数一个很小的值,并且是有用的。
随机梯度下降在深度学习之外有很多重要的应用。它是在大规模数据上训练大型线性模型的主要方法。对于固定大小的模型,每一步随机梯度下降更新的计算量 不取决于训练集的大小 m。在实践中,当训练集大小增长时,我们通常会使用一个更大的模型,但这并非是必须的。达到收敛所需的更新次数通常会随训练集规模增大而增加。然而,当 m 趋向于无穷大时,该模型最终会在随机梯度下降抽样完训练 集上的所有样本之前收敛到可能的最优测试误差。继续增加 m 不会延长达到模型可能的最优测试误差的时间。从这点来看,我们可以认为用 SGD 训练模型的渐近代价是关于 m 的函数的 $O(1)$ 级别。
## 5.10 构建机器学习算法 pipeline
几乎所有的深度学习算法都可以被描述为一个相当简单的 `pipeline`:
1. 特定的数据集
2. 代价函数
3. 优化过程
4. 神经网络模型。
## 参考资料
- 《深度学习》
- [【机器学习】降维——PCA(非常详细)](https://zhuanlan.zhihu.com/p/77151308)
- [更好地理解正则化:可视化模型权重分布](https://zhuanlan.zhihu.com/p/41631717)
================================================
FILE: 1-math_ml_basic/深度学习数学基础-概率与信息论.md
================================================
---
layout: post
title: 深度学习数学基础-概率与信息论
date: 2022-11-01 23:00:00
summary: 概率论是用于表示不确定性声明的数学框架。它不仅提供了量化不确定性的方法,也提供了用于导出新的不确定性声明(statement)的公理。概率论的知识在机器学习和深度学习领域都有广泛应用,是学习这两门学科的基础。
categories: DeepLearning
---
- [前言](#前言)
- [概率论学科定义](#概率论学科定义)
- [概率与信息论在人工智能领域的应用](#概率与信息论在人工智能领域的应用)
- [3.1,为什么要使用概率论](#31为什么要使用概率论)
- [3.2,随机变量](#32随机变量)
- [3.3,概率分布](#33概率分布)
- [3.3.1,离散型变量和概率质量函数](#331离散型变量和概率质量函数)
- [3.3.2,连续型变量和概率密度分布函数](#332连续型变量和概率密度分布函数)
- [3.4,边缘概率](#34边缘概率)
- [3.5,条件概率](#35条件概率)
- [3.5.1,条件概率的链式法则](#351条件概率的链式法则)
- [3.6,独立性和条件独立性](#36独立性和条件独立性)
- [3.7,条件概率、联合概率和边缘概率总结](#37条件概率联合概率和边缘概率总结)
- [3.8,期望、方差和协方差](#38期望方差和协方差)
- [3.8.1,期望](#381期望)
- [期望数学定义](#期望数学定义)
- [期望应用](#期望应用)
- [总体均值数学定义](#总体均值数学定义)
- [3.8.2,方差](#382方差)
- [方差数学定义](#方差数学定义)
- [总体方差数学定义](#总体方差数学定义)
- [3.8.3,期望与方差的运算性质](#383期望与方差的运算性质)
- [3.8.4,协方差](#384协方差)
- [协方差数学定义](#协方差数学定义)
- [3.9,常用概率分布](#39常用概率分布)
- [3.9.1,伯努利分布](#391伯努利分布)
- [3.9.2,Multinoulli 分布](#392multinoulli-分布)
- [3.9.3,高斯分布](#393高斯分布)
- [3.9.4,指数分布和 Laplace 分布](#394指数分布和-laplace-分布)
- [3.10,常用函数的有用性质](#310常用函数的有用性质)
- [3.11,贝叶斯定理](#311贝叶斯定理)
- [3.11.1,贝叶斯定理公式](#3111贝叶斯定理公式)
- [3.11.2,贝叶斯理论与概率密度函数](#3112贝叶斯理论与概率密度函数)
- [3.12,连续型变量的技术细节](#312连续型变量的技术细节)
- [3.13,信息论-相对熵和交叉熵](#313信息论-相对熵和交叉熵)
- [3.14,结构化概率模型](#314结构化概率模型)
- [参考资料](#参考资料)
> 本文内容大多来自《深度学习》(花书)第三章概率与信息论。目录的生成是参考此篇 [文章](https://ecotrust-canada.github.io/markdown-toc/)。
## 前言
### 概率论学科定义
概率论是用于表示**不确定性声明的数学框架**。它不仅提供了量化不确定性的方法,也提供了用于导出新的不确定性**声明**(`statement`)的公理。概率论的知识在机器学习和深度学习领域都有广泛应用,是学习这两门学科的基础。
### 概率与信息论在人工智能领域的应用
在人工智能领域,概率论主要有两种用途。
- 首先,概率定律告诉我们 `AI` 系统应该如何推理,基于此我们设计一些算法来计算或者估算由概率论导出的表达式。
- 其次,我们可以用概率和统计从理论上分析我们提出的 `AI` 系统的行为。
虽然概率论允许我们在存在不确定性的情况下**做出不确定的陈述和推理**,但信息论允许我们量化概率分布中不确定性的数量。
## 3.1,为什么要使用概率论
这是因为机器学习必须始终**处理不确定的量**,有时可能还需要处理随机(非确定性)的量,这里的不确定性和随机性可能来自多个方面。而使用使用概率论来量化不确定性的论据,是来源于 20 世纪 80 年代的 Pearl (1988) 的工作。
不确定性有三种可能的来源:
1. 被建模系统内在的随机性。
2. 不完全观测。
3. 不完全建模:使用了一些必须舍弃某些观测信息的模型。
## 3.2,随机变量
**随机变量**(`random variable`)是可以随机地取不同值的变量,它可以是离散或者连续的。
离散随机变量拥有有限或者可数无限多的状态。注意这些状态不一定非要是整数; 它们也可能只是一些被命名的状态而没有数值。连续随机变量伴随着实数值。注意,随机变量只是对可能状态的描述;它必须与指定这些状态中的每一个的可能性的概率分布相结合。
我们通常用无格式字体 (`plain typeface`) 中的小写字母来表示随机变量本身,而用手写体中的小写字母来表示随机变量能够取到的值。例如, $x_1$ 和 $x_2$ 都是**随机变量** $\textrm{x}$ 可能的取值。对于向量值变量,我们会将随机变量写成 $\mathbf{x}$,它的一个可能取值为 $\boldsymbol{x}$。
> 中文维基百科用 $X$ 表示随机变量,用 $f_{X}(x)$ 表示概率密度函数,本文笔记,不同小节内容两者混用。
## 3.3,概率分布
**概率分布**(`probability distribution`)是用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。
如果狭义地讲,它是指**随机变量的概率分布函数**。具有相同概率分布函数的随机变量一定是相同分布的。连续型和离散型随机变量的**概率分布描述方式**是不同的。
### 3.3.1,离散型变量和概率质量函数
**离散型变量的概率分布可以用概率质量函数**(`probability mass function`, `PMF`,也称概率密度函数)来描述。我们通常用大写字母 $P$ 来表示概率质量函数,用 $\textrm{x} \sim P(\textrm{x})$ **表示随机变量 $\textrm{x}$ 遵循的分布**。
虽然通常每一个随机变量都会有一个不同的概率质量函数,但是概率质量函数也可以同时作用于多个随机变量,这种多个变量的概率分布被称为**联合概率分布**(`joint probability distribution`)。 $P(\textrm{x} = x, \textrm{y} = y)$ 表示 $\textrm{x} = x$ 和 $\textrm{y} = y$ 同时发生的概率,有时也可简写为 $P(x,y)$。
如果一个函数 $P$ 是随机变量 $\textrm{x}$ 的 `PMF`,必须满足以下条件:
+ $P$ 的定义域必须是 $\textrm{x}$ 所有可能状态的集合。
+ $\forall x \in \textrm{x}, 0 \leq P(x)\leq 1$。不可能发生的事件概率为 `0`,能够确保一定发生的事件概率为 `1`。
+ $\sum_{x \in \textrm{x}}P(x)=1$,**归一化**(`normalized`)。
**常见的离散概率分布族有**:
+ 伯努利分布
+ 二项分布:一般用二项分布来计算概率的前提是,每次抽出样品后再放回去,并且只能有两种试验结果,比如黑球或红球,正品或次品等。
+ 几何分布
+ `Poisson` 分布(泊松分布):`Poisson` 近似是二项分布的一种极限形式。
+ 离散均匀分布:即对于随机变量 $\textrm{x}$,因为其是均匀分布(`uniform distribution`),所以它的 `PMF` 为 $P(\textrm{x}=x_{i}) = \frac{1}{k}$,同时 $\sum_{i}P(\textrm{x} = x_{i}) = \sum_{i}\frac{1}{k} = \frac{k}{k} = 1$。
### 3.3.2,连续型变量和概率密度分布函数
**连续型随机变量的概率分布可以用概率密度函数**(`probability desity function, PDF`)来描述。
通常用小写字母 $p$ 来表示随机变量 $\textrm{x}$ 的概率密度函数 `PDF`,其必须满足以下条件:
+ $p$ 的定义域必须是 $\textrm{x}$ 所有可能状态的集合。
+ $\forall x \in \textrm{x}, p(x)\geq 0$。注意,并不要求 $p(x)\leq 1$。
+ $\int p(x)dx=1$。
概率密度函数 $p(x)$ 给出的是落在面积为 $\delta x$ 的无限小的区域内的概率为 $p(x)\delta x$。
因此,我们可以对概率密度函数求积分来获得点集的真实概率质量。特别地,$x$ 落在集合 $\mathbb{S}$ 中的概率可以通过 $p(x)$ 对这个集合求积分来得到。在单变量的例子中,$x$ 落在区间 $[a,b]$ 的概率是 $\int_{[a,b]}p(x)dx$。
**常见的连续概率分布族有**:
+ 均匀分布
+ **正态分布**:连续型随机变量的概率密度函数如下所示。其密度函数的曲线呈对称钟形,因此又被称之为钟形曲线,其中$\mu$ 是平均值,$\sigma$ 是标准差。正态分布是一种理想分布。$${f(x)={\frac {1}{\sigma {\sqrt {2\pi }}}}e^{\left(-{\frac {1}{2}}\left({\frac {x-\mu }{\sigma }}\right)^{2}\right)}}$$
+ 伽玛分布
+ 指数分布
## 3.4,边缘概率
> 边缘概率好像应用并不多,所以这里理解定义和概念即可。
> 边缘概率的通俗理解描述,来源于 [数学篇 - 概率之联合概率、条件概率、边缘概率和贝叶斯法则(笔记)](https://alili.tech/archive/haz1cu03hf/)。
有时候,我们知道了一组变量的联合概率分布,但想要了解其中一个子集的概率分布。这种定义在子集上的概率分布被称为**边缘概率分布**(`marginal probability distribution`)。
对于离散型随机变量 $\textrm{x}$ 和 $\textrm{y}$,知道 $P(\textrm{x}, \textrm{y})$,可以依据下面的**求和法则**(`sum rule`)来计算边缘概率 $P(\textrm{x})$:
$$\forall x \in \textrm{x},P(\textrm{x}=x)=\sum_{y}P(\textrm{x}=x, \textrm{y}=y)$$
“边缘概率”的名称来源于手算边缘概率的计算过程。当 $P(x,y)$ 的每个值被写在由每行表示不同的 $x$ 值,每列表示不同的 $y$ 值形成的网格中时,对网格中的每行求和是很自然的事情,然后将求和的结果 $P(x)$ 写在每行右边的纸的边缘处。
连续性变量的边缘概率则用积分代替求和:
$$p(x) = \int p(x,y)dy$$
## 3.5,条件概率
**条件概率(`conditional probability`)就是事件 A 在事件 B 发生的条件下发生的概率**,表示为 $P(A|B)$。
设 $A$ 与 $B$ 为样本空间 Ω 中的两个事件,其中 $P(B)$ > 0。那么在事件 $B$ 发生的条件下,事件 $A$ 发生的条件概率为:
$$P(A|B)={\frac {P(A\cap B)}{P(B)}}$$
> 花书中期望的条件概率定义(表达式不一样,但意义是一样的,维基百科的定义更容易理解名字意义,花书中的公式更多的是从数学中表达):
> 将给定 $\textrm{x} = x$ 时, $\textrm{y} = y$ 发生的条件概率记为 $P(\textrm{y} = y|\textrm{x} = x)$,这个条件概率的计算公式如下:
> $$P(\textrm{y}=y|\textrm{x}=x)=\frac{P(\textrm{y}=y, \textrm{x}=x)}{P(\textrm{x}=x)}$$
> 条件概率只在 $P(\textrm{x}=x)\geq 0$ 时有定义,即不能计算以从未发生的事件为条件的条件概率。
### 3.5.1,条件概率的链式法则
任何多维随机变量的联合概率分布,都可以分解成只有一个变量的条件概率相乘的形式,这个规则被称为概率的**链式法则**(`chain rule`)。条件概率的链式法则如下:
$$
\begin{align}
P(a,b,c) &= P(a|b,c)P(b,c) \nonumber \\
P(b,c) &= P(b|c)P(c) \nonumber \\
P(a,b,c) &= P(s|b,c)P(b|c)P(c) \nonumber
\end{align}
$$
### 3.6,独立性和条件独立性
两个随机变量 $\textrm{x}$ 和 $\textrm{y}$,如果它们的概率分布可以表示成两个因子的乘积形式,并且一个因子只包含 $\textrm{x}$ 另一个因子只包含 $\textrm{y}$,我们就称这两个随机变量是**相互独立**的(`independent`):
$$\forall x \in \textrm{x},y \in \textrm{y},p(\textrm{x}=x, \textrm{y}=y)=p(\textrm{x}=x)\cdot p(\textrm{y}=y)$$
两个相互独立的随机变量同时发生的概率可以通过各自发生的概率的乘积得到。
如果关于 $x$ 和 $y$ 的条件概率分布对于 $z$ 的每一个值都可以写成乘积的形式,那么这两个随机变量 $x$ 和 $y$ 在给定随机变量 $z$ 时是条件独立的(conditionally independent):
$$\forall x \in ,y \in \textrm{y},z \in \textrm{z}, p(\textrm{x}=x, \textrm{y}=y|z \in \textrm{z})= p(\textrm{x}=x|z \in \textrm{z})\cdot p(\textrm{y}=y|z \in \textrm{z})$$
采用一种简化形式来表示独立性和条件独立性: $\textrm{x}\perp\textrm{y}$ 表示 $\textrm{x}$ 和 $\textrm{y}$ 相互独立,$\textrm{x}\perp\textrm{y}\vert\textrm{z}$ 表示 $\textrm{x}$ 和 $\textrm{y}$ 在给定 $\textrm{z}$ 时条件独立。
## 3.7,条件概率、联合概率和边缘概率总结
1. **条件概率(`conditional probability`)就是事件 A 在事件 B 发生的条件下发生的概率**。条件概率表示为 $P(A\vert B)$,读作“A 在 B 发生的条件下发生的概率”。
2. 联合概率表示两个事件共同发生的概率。`A` 与 `B` 的联合概率表示为 $P(A\cap B)$ 或者 $P(A,B)$ 或者 $P(AB)$。
3. 仅与单个随机变量有关的概率称为边缘概率。
## 3.8,期望、方差和协方差
> 为了便于理解,本章中的期望和方差的数学定义主要采用中文维基百科中的定义。
在概率分布中,期望值和方差或标准差是一种分布的重要特征,期望、数学期望、均值都是一个意思。统计中的方差(样本方差)是每个样本值与全体样本值的平均数之差的平方值的平均数,其意义和概率分布中的方差是不一样的。
### 3.8.1,期望
在概率论和统计学中,一个离散性随机变量的期望值(或数学期望,亦简称期望,物理学中称为期待值)是试验中**每次可能的结果乘以其结果概率的总和**。换句话说,期望值像是随机试验在同样的机会下重复多次,所有那些可能状态平均的结果,也可理解为**该变量输出值的加权平均**。
#### 期望数学定义
如果 $X$ 是在概率空间 $(\Omega ,F,P)$ 中的随机变量,那么它的期望值 $\operatorname{E}(X)$ 的定义是:
$$\operatorname {E}(X)=\int_{\Omega }X {d}P$$
**并不是每一个随机变量都有期望值的,因为有的时候上述积分不存在。如果两个随机变量的分布相同,则它们的期望值也相同**。
1,如果 $X$ 是**离散的随机变量**,输出值为 $x_{1},x_{2},\ldots x_{1},x_{2},\ldots$,和输出值相应的概率为 ${\displaystyle p_{1},p_{2},\ldots }p_{1},p_{2},\ldots$(概率和为 `1`)。
若级数 $\sum_{i}p_{i}x_{i}$ 绝对收敛,那么期望值 $\operatorname {E}(X)$ 是一个无限数列的和。
$$\operatorname {E}(X)=\sum_{i}p_{i}x_{i}$$
2,如果 $X$ 是**连续的随机变量**,且存在一个相应的概率密度函数 $f(x)$,若积分 $\int _{-\infty }^{\infty }xf(x)\,\mathrm {d} x$ 绝对收敛,那么 $X$ 的期望值可以计算为:
$$\operatorname {E} (X)=\int _{-\infty }^{\infty }xf(x)\,\mathrm {d} x$$
虽然是针对于连续的随机变量的,但与离散随机变量的期望值的计算算法却同出一辙,由于输出值是连续的,所以**只是把求和改成了积分**。
**期望值 $E$ 是线性函数**:
$$\operatorname {E}(aX+bY)=a\operatorname {E}(X)+b\operatorname {E}(Y)$$
$X$ 和 $Y$ 为**在同一概率空间的两个随机变量**(可以独立或者非独立),$a$ 和 $b$ 为任意实数。
> 花书中期望的数学定义(表达式不一样,但意义是一样的):
>
> 1,某个函数 $f(x)$ 相对于概率分布 $P(x)$ 的期望(期望值)是当从 $P$ 中抽取 $x$ 时 $f$ 所取的平均或平均值。对于离散型随机变量,期望可以通过**求和**得到:
> $$\mathbb{E}_{\textrm{x}\sim P}[f(x)] = \sum_{x} P(x)f(x)$$
>
> 2,对于连续型随机变量可以通过求**积分**得到:
> $$\mathbb {E}_{\textrm{x}\sim p}[f(x)] = \int p(x)f(x)dx$$
#### 期望应用
1. 在**统计学**中,估算变量的期望值时,经常用到的方法是重复测量此变量的值,再用所得数据的平均值来估计此变量的期望值。
2. 在**概率分布**中,期望值和方差或标准差是一种分布的重要特征。
#### 总体均值数学定义
一般而言,一个有限的容量为 $N$、元素的值为 $x_{i}$ 的总体的总体均值为:
$$\mu = \frac{\sum_i^N x_{i}}{N}$$
### 3.8.2,方差
在概率论和统计学中,方差(英语:`variance`)又称变异数、变方,描述的是**一个随机变量的离散程度,即该变量离其期望值的距离**,是随机变量与其总体均值或样本均值的离差的平方的期望值。
方差差是标准差的平方、分布的二阶矩,以及随机变量与其自身的协方差,其常用的符号表示有 $\sigma^2$、$s^2$、$\operatorname {Var} (X)$、$\displaystyle V(X)$,以及 $\displaystyle \mathbb {V} (X)$。
方差作为离散度量的优点是,它比其他离散度量(如平均差)更易于代数运算,但缺点是它与随机变量的单位不同,而标准差则单位相同,这就是计算完成后通常采用标准差来衡量离散程度的原因。
> 方差的正平方根称为该随机变量的标准差。
有两个不同的概念都被称为“方差”。一种如上所述,是理论概率分布的方差。而另一种方差是一组观测值的特征,分别是**总体方差**(所有可能的观测)和**样本方差**(总体的一个子集)。
#### 方差数学定义
设 $X$ 为服从分布 $F$ 的随机变量,如果 $\operatorname{E}[X]$ 是随机变量 $X$ 的期望值(均值 $\mu=\operatorname{E}[X]$),则随机变量 $X$ 或者分布 $F$ 的**方差**为 $X$ 的**离差平方的期望值**:
$$\operatorname{E}(X) = \operatorname{E}[(X - \mu)]^2 = \operatorname{E}[X - \operatorname{E}(X)]^2$$
方差的表达式可展开如下:
$$
\begin{align}
\operatorname{Var}(X) &=\operatorname{E} \left[(X-\operatorname {E} [X])^{2}\right] \nonumber \\
&=\operatorname{E} \left[X^{2}-2X\operatorname {E} [X]+\operatorname{E}[X]^{2}\right] \nonumber \\
&=\operatorname{E} \left[X^{2}\right]-2\operatorname{E}[X]\operatorname{E}[X]+\operatorname{E}[X]^{2} \nonumber \\
&=\operatorname{E} \left[X^{2}\right]-\operatorname{E}[X]^{2} \nonumber \\
\end{align}
$$
也就是说,$X$ 的方差等于 $X$ 平方的均值减去 $X$ 均值的平方。
#### 总体方差数学定义
一般而言,一个有限的容量为 $N$、元素的值为 $x_{i}$ 的总体的总体方差为:
$$\sigma^{2} = {\frac {1}{N}}\sum _{i=1}^{N}\left(x_{i}-\mu \right)^{2}$$
> 花书中方差的定义: **方差**(`variance`)衡量的是当我们对 $x$ 依据它的概率分布进行采样时,随机变量 $\textrm{x}$ 的函数值会呈现多大的差异,或者说一个随机变量的方差描述的是它的离散程度,也就是该变量离其期望值的距离。方差定义如下:
> $$Var(f(x)) = \mathbb{E}[(f(x) - \mathbb{E}[f(x)])^2]$$
### 3.8.3,期望与方差的运算性质
**期望与方差运算性质**如下:


> 来源: 知乎文章-[【AP统计】期望E(X)与方差Var(X)](https://zhuanlan.zhihu.com/p/64859161)。
### 3.8.4,协方差
协方差也叫共变异数(英语:Covariance),在概率论与统计学中用于**衡量两个随机变量的联合变化程度**。
#### 协方差数学定义
期望值分别为 $\operatorname E(X)=\mu$ 与 $\operatorname E(Y)=\nu$ 的两个具有有限二阶矩的实数随机变量 $X$ 与 $Y$ 之间的协方差定义为:
$$\operatorname {cov} (X,Y)=\operatorname {E} ((X-\mu )(Y-\nu ))=\operatorname {E} (X\cdot Y)-\mu \nu$$
协方差表示的是两个变量的总体的误差,这与只表示一个变量误差的方差不同。
协方差的绝对值如果很大则意味着变量值变化很大并且它们同时距离各自的均值很 远。如果协方差是正的,那么两个变量都倾向于同时取得相对较大的值。如果协方 差是负的,那么其中一个变量倾向于取得相对较大的值的同时,另一个变量倾向于 取得相对较小的值,反之亦然。其他的衡量指标如 相关系数(`correlation`)将每个变 量的贡献归一化,为了只衡量变量的相关性而不受各个变量尺度大小的影响。
## 3.9,常用概率分布
下表列出了一些常用概率分布的方差。

### 3.9.1,伯努利分布
**伯努利分布**(英语:`Bernoulli distribution`),又名两点分布或者 `0-1` 分布,是一个离散型概率分布,为纪念瑞士科学家雅各布·伯努利而命名。若伯努利试验成功,则伯努利随机变量取值为 `1`。若伯努利试验失败,则伯努利随机变量取值为 `0`。记其成功概率为 $0\leq p\leq 1$,失败概率为 $q = 1-p$。其有如下性质:
1. 其**概率质量函数**为:
$$
f_{X}(x) = p^{x}(1-p)^{1-x} = \left\lbrace\begin{matrix}
p \quad if \;x = 1 \\
1-p \quad if \; x = 0
\end{matrix}\right.
$$
2. 其**期望值**为:
$$\operatorname {E} [X] = \sum_{i=0}^{1} x_{i}f_X(x) = 0 + p = p$$
3. 其**方差**为:
$$\begin{aligned}
Var[X] &= \sum_{i=0}^{1} (x_{i}-\operatorname {E} [X])^2f_{X}(x) \\
&= (0-P)^2(1-P) + (1-P)^2P \\
&= p(1-p) \\
&= p\cdot q \\
\end{aligned}$$
### 3.9.2,Multinoulli 分布
`Multinoulli` 分布(多项式分布,也叫范畴分布 `categorical dis- tribution`)是一种离散概率分布,它描述了随机变量的可能结果,该随机变量可以采用 $k$ 个可能类别之一,概率为每个类别分别指定,其中 $k$ 是一个有限值。
### 3.9.3,高斯分布
> 有几种不同的方法用来说明一个随机变量。最直观的方法是**概率密度函数**,这种方法能够表示随机变量每个取值有多大的可能性。
高斯分布 `Gaussian distribution`(也称正态分布 `Normal distribution`)是一个非常常见的**连续概率分布**。高斯分布在统计学上十分重要,经常用在自然和社会科学来代表一个不确定的随机变量。
若随机变量 $X$ 服从一个位置参数为 $\mu$ 、尺度参数为 $\sigma$ 的正态分布,记为:
$$X \sim N(\mu,\sigma^2)$$
则其**概率密度函数**为 $$f(x;\mu, \sigma) = \frac {1}{\sigma {\sqrt {2\pi }}}\;e^{-{\frac {\left(x-\mu \right)^{2}}{2\sigma ^{2}}}}$$
正态分布的数学期望值 $\mu$ 等于位置参数,决定了分布的**位置**;其方差 $\sigma^2$ 的开平方或标准差 $\sigma$ 等于尺度参数,决定了分布的**幅度**。
正态分布概率密度函数曲线呈钟形,也称之为钟形曲线(类似于寺庙里的大钟,因此得名)。我们通常所说的**标准常态分布**是位置参数 $\mu = 0$,尺度参数 $\sigma ^{2} = 1$ 的正态分布(见右图中红色曲线)。

采用正态分布在很多应用中都是一个明智的选择。当我们由于缺乏关于某个实 数上分布的先验知识而不知道该选择怎样的形式时,正态分布是默认的比较好的选择,其中有两个原因。
1. 第一,我们想要建模的很多分布的真实情况是比较接近正态分布的。
2. 第二,在具有相同方差的所有可能的概率分布中,正态分布在实数上具有最 的不确定性。因此,我们可以认为正态分布是对模型加入的先验知识量最少的分布。
### 3.9.4,指数分布和 Laplace 分布
在概率论和统计学中,**指数分布**(`Exponential distribution`)是一种连续概率分布,表示一个在 $x = 0$ 点处取得边界点 (`sharp point`) 的分布,其使用指示函数(`indicator function`) $1_{x\geq0}$ 来使得当 $x$ 取负值时的概率为零。指数分布可以等同于形状母数 $\alpha$为 $1$的**伽玛分布**。
指数分布可以用来表示独立随机事件发生的时间间隔,比如旅客进入机场的时间间隔、电话打进客服中心的时间间隔等。
若随机变量 $X$ 服从母数为 $\lambda$ 或 $\beta$ 的指数分布,则记作
$X\sim {\text{Exp}}(\lambda )$ 或 $X\sim {\text{Exp}}(\beta )$
两者意义相同,只是 $\lambda$ 与 $\beta$ 互为倒数关系。**指数分布的概率密度函数**为:
$$
f(x;{\color {Red}\lambda })=\left\lbrace{\begin{matrix}{\color {Red}\lambda }e^{-{\color {Red}\lambda }x}&x\geq 0,\\0&,\;x<0.\end{matrix}}\right.
$$
**指数分配概率密度函数曲线**如下所示。

## 3.10,常用函数的有用性质
深度学习中的概率分布有一些经常出现的函数,比如 `logistic sigmoid` 函数:
$$\sigma(x) = \frac{1}{1+exp(-x)}$$
`logistic sigmoid` 函数通常用来产生伯努利分布的参数 $p$,因为它的范围是 $(0, 1)$,位于 $p$ 参数值的有效范围内。下图 3.3 给出了 `sigmoid` 函数的图示。从图中可以明显看出,`sigmoid` 函数在变量取绝对值非常大的正值或负值时会出现**饱和(`saturate`)现象**,意味着函数会变得**很平**,并且对输入的微小改变会变得**不敏感**。

`sigmoid` 函数的一些性质在后续学习 `BP` 算法等内容时会很有用,我们需要牢记:
$$
\begin{align}
\sigma(x) &= \frac{exp(x)}{exp(x)+exp(0)} \nonumber \\
\frac{d}{dx}\sigma(x) &= \sigma(x)(1 - \sigma(x)) \nonumber \\
1 - \sigma(x) &= \sigma(-x) \nonumber \\
\end{align}
$$
## 3.11,贝叶斯定理
> 本小节只是简单介绍基本概念和公式,更全面和深入的理解建议看《机器学习》书籍。
贝叶斯定理(英语:`Bayes' theorem`)是概率论中的一个定理,描述**在已知一些条件下,某事件的发生概率**。比如,如果已知某种健康问题与寿命有关,使用贝叶斯定理则可以通过得知某人年龄,来更加准确地计算出某人有某种健康问题的概率。
通常,事件 A 在事件 B 已发生的条件下发生的概率,与事件 B 在事件 A 已发生的条件下发生的概率是不一样的。但是,这两者是有确定的关系的,贝叶斯定理就是这种关系的陈述。贝叶斯公式的一个用途,即透过已知的三个概率而推出第四个概率。贝叶斯定理跟随机变量的条件概率以及边际概率分布有关。
作为一个普遍的原理,贝叶斯定理对于所有概率的解释是有效的。这一定理的主要应用为贝叶斯推断,是推论统计学中的一种推断法。这一定理名称来自于托马斯·贝叶斯。
> 来源[中文维基百科-贝叶斯定理](https://zh.wikipedia.org/wiki/%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%AE%9A%E7%90%86)
### 3.11.1,贝叶斯定理公式
贝叶斯定理是关于随机事件 A 和 B 的条件概率的一则定理。
$$P(A\mid B)={\frac {P(A)P(B\mid A)}{P(B)}}$$
其中 A 以及 B 为随机事件,且 $P(B)$ 不为零。$P(A\mid B)$ 是指在事件 B 发生的情况下事件 A 发生的概率。
在贝叶斯定理中,每个名词都有约定俗成的名称:
- $P(A\mid B)$ 是已知 B 发生后,A 的**条件概率**。也称作 A 的事后概率。
- $P(A)$ 是 A 的先验概率(或边缘概率)。其不考虑任何 B 方面的因素。
- $P(B\mid A)$ 是已知 A 发生后,B 的条件概率。也可称为 B 的后验概率。某些文献又称其为在特定 B 时,A 的似然性,因为 $P(B\mid A)=L(A\mid B)$。
- $P(B)$是 B 的**先验概率**。
### 3.11.2,贝叶斯理论与概率密度函数
贝叶斯理论亦可用于概率分布,贝叶斯理论与概率密度的关系是由求极限的方式建立:
$$P(\textrm{x}|\textrm{y}) = \frac{P(\textrm{x})P(\textrm{y}|\textrm{x})}{P(\textrm{y})}$$
注意到 $P(y)$ 出现在上面的公式中,它通常使用 $P(\textrm{y}) = \sum_{x} P(\textrm{y}|x)P(x)$ 来计算所以我们并不需要事先知道 $P(\textrm{y})$ 的信息。
> 中文维基百科中贝叶斯理论与概率密度关系定义:
> $$f(x|y)={\frac {f(x,y)}{f(y)}}={\frac {f(y|x)\,f(x)}{f(y)}}$$
## 3.12,连续型变量的技术细节
连续型随机变量和概率密度函数的深入理解需要用到数学分支**测度论**(`measure theory`)的相关内容来扩展概率论,测度论超出了本文范畴。《深度学习》原书中有测度论的简要介绍,本笔记不做记录和摘抄,感兴趣的可以阅读原书。
## 3.13,信息论-相对熵和交叉熵
信息论是应用数学、电子学和计算机科学的一个分支,早期备用在无线通信领域。在深度学习中,主要是使用信息论的一些关键思想来**表征(`characterize`)概率分布**或者**量化概率分布之间的相似性**。
信息论的基本想法是一个不太可能的事件居然发生了,要比一个非常可能的事件发生,能提供更多的信息。
定义一个事件 $\textrm{x} = x$ 的自信息(self-information) 为
$$
I(x) = -\text{log}P(x)
$$
在本文中,我们总是用 $\text{log}$ 来表示自然对数,其底数为 $e$。因此我们定义的 $I(x)$ 单位是**奈特**(nats)。一奈特是以 $\frac{1}{e}$ 的概率观测到一个事件时获得的信息量。其他的材料中可能使用底数为 2 的对数,单位是**比特**(bit)或者香农(shannons); 通过比特度量的信息只是通过奈特度量信息的常数倍。
自信息只处理单个的输出。我们可以用**香农熵**(`Shannon entropy`)来对整个概率分布中的不确定性总量进行量化:
$$H(P) = H(\textrm{x}) = E_{x∼P}[I(x)] = −E_{x∼P}[log P(x)]$$
换句话说,一个概率分布的香农熵是指遵循这个分布的事件所产生的期望信息总量。
如果我们对于同一个随机变量 $\textrm{x}$ 有两个单独的概率分布 $P(\textrm{x})$ 和 $Q(\textrm{x})$,则可以用 **KL 散度**( `Kullback-Leibler (KL) divergence`,也叫相对熵)来**衡量这两个概率分布的差异**:
$$D_{KL}(P\parallel Q) = \mathbb{E}_{\textrm{x}\sim p}\begin{bmatrix}
log \frac{P(x)}{Q(x)} \end{bmatrix} = \mathbb{E}_{\textrm{x}\sim p}[log P(x) - log Q(x)]$$
KL 散度有很多有用的性质,最重要的是它是非负的。KL 散度为 0 当且仅当 $P$ 和 $Q$ 在离散型变量的情况下是相同的概率分布,或者在连续型变量的情况下是 “几乎处处” 相同的。
一个和 KL 散度密切联系的量是**交叉熵**(`cross-entropy`)$H(P, Q) = H(P) + D_{KL}(P\vert\vert Q)$,其计算公式如下:
$$H(P, Q) = -\mathbb{E}_{\textrm{x}\sim p}log Q(x)$$
和 KL 散度相比,少了左边一项,即熵 $H(P)$。可以看出,最小化 KL 散度其实就是在最小化分布之间的交叉熵。
> 上式的写法是在前面所学内容**数学期望**的基础上给出的,还有一个写法是《机器学习-周志华》书中附录 C 中给出的公式,更为直观理解:
> $$KL(P\parallel Q) = \int_{-\infty }^{+\infty} p(x)log \frac{p(x)}{q(x)} dx$$
> 其中 $p(x)$ 和 $q(x)$ 分别为 $P$ 和 $Q$ 的概率密度函数。
> 这里假设两个分布均为连续型概率分布,对于离散型概率分布,只需要将积分替换为对所有离散值遍历求和。
> `KL` 散度满足非负性和不满足对称性。将上式展开可得:
> $$\text{KL 散度} KL(P\parallel Q) = \int_{-\infty }^{+\infty}p(x)logp(x)dx - \int_{-\infty }^{+\infty}p(x) logq(x)dx = -H(P) + H(P,Q)$$
> $$\text{交叉熵} H(P,Q) = \mathbb{E}_{\textrm{x}\sim p} log Q(x) = - \int_{-\infty }^{+\infty} p(x) logq(x)dx$$
> 其中,$H(P)$ 为熵(`entropy`),$H(P,Q)$ 为交叉熵(`cross entropy`)。
>在信息论中,熵 $H(P)$ 表示对来自 $P$ 的随机遍历进行编码所需的最小字节数,而交叉熵 $H(P,Q)$ 表示使用 $Q$ 的编码对来自 $P$ 的变量进行编码所需的字节数。因此 KL 散度可认为是使用基于 $Q$ 的编码对来自 $P$ 的变量进行编码所需的“额外字节数”;显然,额外字节数非负,当且仅当 $P=Q$ 时额外字节数为 `0`。
## 3.14,结构化概率模型
略
## 参考资料
+ https://zh.m.wikipedia.org/zh-hans/%E6%96%B9%E5%B7%AE#
+ 《深度学习》
+ 《机器学习》
================================================
FILE: 1-math_ml_basic/随机梯度下降法的数学基础.md
================================================
---
layout: post
title: 随机梯度下降法的数学基础
date: 2023-01-20 23:00:00
summary: 本文从导数开始讲起,讲述了导数、偏导数、方向导数和梯度的定义、意义和数学公式,有助于初学者后续更深入理解随机梯度下降算法的公式。大部分内容来自维基百科和博客文章内容的总结,并加以个人理解。
categories: DeepLearning
---
> 梯度是微积分中的基本概念,也是机器学习解优化问题经常使用的数学工具(梯度下降算法)。因此,有必要从头理解梯度的来源和意义。本文从导数开始讲起,讲述了导数、偏导数、方向导数和梯度的定义、意义和数学公式,有助于初学者后续更深入理解随机梯度下降算法的公式。大部分内容来自维基百科和博客文章内容的总结,并加以个人理解。
## 导数
导数(英语:`derivative`)是微积分学中的一个概念。函数在某一点的导数是指这个函数在这一点附近的**变化率**。导数的本质是通过极限的概念对函数进行局部的线性逼近。当函数 $f$ 的自变量在一点 $x_0$ 处产生一个增量时 $h$ 时,函数输出值的增量与自变量增量 $h$ 的比值在 $h$ 趋于 0 时的极限如果存在,则将这个比值定义为 $f$ 在 $x_0$ 处的导数,记作 ${f}'(x_0)$、$\frac{\mathrm{d}f}{\mathrm{d}x}(x_0)$ 或 $\frac{\mathrm{d}f}{\mathrm{d}x}\vert_{x=x_0}$
导数是函数的局部性质。不是所有的函数都有导数,一个函数也不一定在所有的点上都有导数。若某函数在某一点导数存在,则称其在这一点可导(可微分),否则称为不可导(不可微分)。如果函数的自变量和取值都是实数的话,那么函数在某一点的导数就是该函数所代表的曲线在这一点上的切线斜率。
<center>
<img src="../images/bp/curve_slope.png" width="50%" alt="导数曲线">
</center>
对于可导的函数 $f$,$x \mapsto f'(x)$ 也是一个函数,称作 $f$ 的导函数。导数示例如下图所示:
<center>
<img src="../images/bp/Tangent_function_animation.gif" width="50%" alt="函数每个位置处的导数">
</center>
导数的一般定义如下:
如果实函数 $f$ 在点 $a$ 的某个领域内有定义,且以下极限(注意这个表达式所定义的函数定义域不含 $a$ )
$$
{\displaystyle \lim _{x\to a}{\frac {f(x)-f(a)}{x-a}}}
$$
存在,则称 $f$ 于 $a$ 处可导,并称这个极限值为 $f$于$a$ 处的导数,记作 $f'(a)$。
## 常用初等函数的导数公式

## 偏导数
> 偏导数的作用与价值在向量分析和微分几何以及机器学习领域中受到广泛认可。
导数是一元函数的变化率(斜率),导数也是函数,可以理解为函数的变化率与位置的关系。
那么如果是多元函数的变化率问题呢?答案是**偏导数**,定义为**多元函数沿坐标轴的变化率**。
偏导数是多元函数“退化”成一元函数时的导数,这里“退化”的意思是固定其他变量的值,只保留一个变量,依次保留每个变量,则 $N$ 元函数有 $N$ 个偏导数。
如果一个变量对应一个坐标轴,那么偏导数可以理解为函数在每个位置处沿着自变量坐标轴方向上的导数(切线斜率)。
在数学中,偏导数(英语:`partial derivative`)的定义是:一个**多变量**的函数(或称多元函数),**对其中一个变量(导数)微分,而保持其他变量恒定**。函数 $f$ 关于变量 $x$ 的偏导数记为 $f'(x)$ 或 $\frac{\partial f}{\partial x}$。偏导数符号 $\partial $ 是全导数符号 $d$ 的变体。
假设 $f$ 是一个多元函数。例如:
$$z = f(x, y) = x^2 + xy + y^2$$
我们把变量 $y$ 视为常数,通过对方程求导,我们可以得到**函数 $f$ 关于变量 $x$ 的偏导数**:
$$\displaystyle {\frac {\partial f}{\partial x}} = 2x + y$$
同理可得,函数 $f$ 关于变量 $y$ 的偏导数:
$$\frac {\partial f}{\partial y} = x + 2y$$
## 方向导数
在前面导数和偏导数的定义中,均是**沿坐标轴正方向讨论函数的变化率**。那么当我们讨论函数沿任意方向的变化率时,也就引出了方向导数的定义,即:**某一点在某一趋近方向上的导数值**。
通俗理解就是:我们不仅要知道函数在坐标轴正方向上的变化率(即偏导数),而且还要设法求得函数在其他特定方向上的变化率(方向导数)。如下图所示,点 $P$ 位置处红色箭头方向的方向导数为黑色切线的斜率。图片来自链接 [Directional Derivative](https://www.geogebra.org/m/Bx8nFMNc)。
<center>
<img src="../images/bp/Directional_Derivative_Visual.png" width="70%" alt="Directional Derivative Visual">
</center>
方向导数的定义参考下图,来源-[直观理解梯度,以及偏导数、方向导数和法向量等](https://www.cnblogs.com/shine-lee/p/11715033.html)。
<center>
<img src="../images/bp/Direction_Derivative_Definition.png" width="70%" alt="方向导数计算推导">

</center>
## 梯度
在向量微积分中,梯度(英语:`gradient`)是一种关于多元导数的概括。平常的一元(单变量)函数的导数是标量值函数,而多元函数的梯度是**向量值**函数。
就像一元函数的导数表示这个函数图形的切线的斜率,如果多元函数在点 $P$ 上的梯度不是零向量,则它的方向是这个函数在 $P$ 上最大增长的方向、而它的量是在这个方向上的**增长率**。
梯度,写作 $\nabla f$ 或 grad $f$,二元时为($\frac{\partial f(x,y)}{\partial x}, \frac{\partial f(x,y)}{\partial y}$)。梯度是微积分中的基本概念,也是机器学习解优化问题经常使用的数学工具(梯度下降算法)。借助前面方向导数的推导公式,我们可以得到 $xy$ 平面上一点 $(a,b)$ 处 $\theta$ 方向上的方向导数和其意义如下图:

可以从以下两个实例理解**梯度意义**:
1. 假设有一个房间,房间内所有点的温度由一个标量场 $\phi$ 给出的,即点 $(x,y,z)$ 的温度是 $\phi(x,y,z)$。假设温度不随时间改变。然后,在房间的每一点,该点的梯度将显示变热最快的方向。梯度的大小将表示在该方向上的温度变化率。
2. 考虑一座高度函数为 $H$ 的山,山上某点 $(x, y)$ 的高度是 $H(x, y)$,点 $(x,y)$ 的梯度是在该点坡度(或者说斜度)最陡的方向。梯度的大小会告诉我们坡度到底有多陡。
总结梯度的几何意义:
- 当前位置的**梯度方向**,为函数在该位置处方向导数最大的方向,也是函数值上升最快的方向,反方向为下降最快的方向;
- 当前位置的**梯度长度**(模),为最大方向导数的值。
## 总结
- 方向导数是各个方向上的导数。
- 偏导数连续才有梯度存在。
- 偏导数构成的向量为梯度。
- **梯度的方向是方向导数中取到最大值的方向**,梯度的值是方向导数的最大值。
## 参考资料
1. [维基百科-偏导数](https://zh.m.wikipedia.org/zh-hans/%E5%81%8F%E5%AF%BC%E6%95%B0)
2. [直观理解梯度,以及偏导数、方向导数和法向量等](https://www.cnblogs.com/shine-lee/p/11715033.html)
3. [AI-EDU: 09.4 神经网络非线性回归的实现](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC4%E6%AD%A5%20-%20%E9%9D%9E%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/09.4-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E9%9D%9E%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92%E7%9A%84%E5%AE%9E%E7%8E%B0.html)
4. [如何理解梯度下降法?](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)
================================================
FILE: 2-deep_learning_basic/cnn基础部件-BN层详解.md
================================================
- [一,数学基础](#一数学基础)
- [1.1,概率密度函数](#11概率密度函数)
- [1.2,正态分布](#12正态分布)
- [二,背景](#二背景)
- [2.1,如何理解 Internal Covariate Shift](#21如何理解-internal-covariate-shift)
- [2.2,Internal Covariate Shift 带来的问题](#22internal-covariate-shift-带来的问题)
- [2.3,减少 Internal Covariate Shift 的一些尝试](#23减少-internal-covariate-shift-的一些尝试)
- [三,批量归一化(BN)](#三批量归一化bn)
- [3.1,BN 的前向计算](#31bn-的前向计算)
- [3.2,BN 层如何工作](#32bn-层如何工作)
- [3.3,推理时的 BN 层](#33推理时的-bn-层)
- [3.4,实验](#34实验)
- [3.5,BN 层的作用](#35bn-层的作用)
- [参考资料](#参考资料)
## 一,数学基础
### 1.1,概率密度函数
随机变量(random variable)是可以随机地取不同值的变量。随机变量可以是离散的或者连续的。简单起见,本文用大写字母 $X$ 表示随机变量,小写字母 $x$ 表示随机变量能够取到的值。例如,$x_1$ 和 $x_2$ 都是随机变量 $X$ 可能的取值。随机变量必须伴随着一个概率分布来指定每个状态的可能性。
概率分布(probability distribution)用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。我们描述概率分布的方式取决于随机变量是离散的还是连续的。
当我们研究的对象是连续型随机变量时,我们用**概率密度函数**(probability density function, `PDF`)而不是概率质量函数来描述它的概率分布。
> 更多内容请阅读《花书》第三章-概率与信息论,或者我的文章-[深度学习数学基础-概率与信息论](https://github.com/HarleysZhang/deep_learning_alchemy/blob/main/1-math_ml_basic/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E6%95%B0%E5%AD%A6%E5%9F%BA%E7%A1%80-%E6%A6%82%E7%8E%87%E4%B8%8E%E4%BF%A1%E6%81%AF%E8%AE%BA.md)。
### 1.2,正态分布
> 当我们不知道数据真实分布时使用正态分布的原因之一是,正态分布拥有最大的熵,我们通过这个假设来施加尽可能少的结构。
实数上最常用的分布就是正态分布(normal distribution),也称为高斯分布 (Gaussian distribution)。
如果随机变量 $X$ ,服从位置参数为 $\mu$、尺度参数为 $\sigma$ 的概率分布,且其概率密度函数为:
$$
f(x)=\frac{1}{\sigma\sqrt{2 \pi} } e^{- \frac{{(x-\mu)^2}}{2\sigma^2}} \tag{1}
$$
则这个随机变量就称为正态随机变量,正态随机变量服从的概率分布就称**为正态分布**,记作:
$$
X \sim N(\mu,\sigma^2) \tag{2}
$$
如果位置参数 $\mu = 0$,尺度参数 $\sigma = 1$ 时,则称为标准正态分布,记作:
$$
X \sim N(0, 1) \tag{3}
$$
此时,概率密度函数公式简化为:
$$
f(x)=\frac{1}{\sqrt{2 \pi}} e^{- \frac{x^2}{2}} \tag{4}
$$
正态分布的数学期望值或期望值 $\mu$ 等于位置参数,决定了分布的位置;其方差 $\sigma^2$ 的开平方或标准差 $\sigma$ 等于尺度参数,决定了分布的幅度。正态分布的概率密度函数曲线呈钟形,常称之为**钟形曲线**,如下图所示:

可视化正态分布,可直接通过 `np.random.normal` 函数生成指定均值和标准差的正态分布随机数,然后基于 `matplotlib + seaborn` 库 `kdeplot`函数绘制概率密度曲线。示例代码如下所示:
```py
import seaborn as sns
x1 = np.random.normal(0, 1, 100)
x2 = np.random.normal(0, 1.5, 100)
x3 = np.random.normal(2, 1.5, 100)
plt.figure(dpi = 200)
sns.kdeplot(x1, label="μ=0, σ=1")
sns.kdeplot(x2, label="μ=0, σ=1.5")
sns.kdeplot(x3, label="μ=2, σ=2.5")
#显示图例
plt.legend()
#添加标题
plt.title("Normal distribution")
plt.show()
```
以上代码直接运行后,输出结果如下图:

当然也可以自己实现正态分布的概率密度函数,代码和程序输出结果如下:
```python
import numpy as np
import matplotlib.pyplot as plt
plt.figure(dpi = 200)
plt.style.use('seaborn-darkgrid') # 主题设置
def nd_func(x, sigma, mu):
"""自定义实现正态分布的概率密度函数
"""
a = - (x-mu)**2 / (2*sigma*sigma)
f = np.exp(a) / (sigma * np.sqrt(2*np.pi))
return f
if __name__ == '__main__':
x = np.linspace(-5, 5)
f = nd_fun(x, 1, 0)
p1, = plt.plot(x, f)
f = nd_fun(x, 1.5, 0)
p2, = plt.plot(x, f)
f = nd_fun(x, 1.5, 2)
p3, = plt.plot(x, f)
plt.legend([p1 ,p2, p3], ["μ=0,σ=1", "μ=0,σ=1.5", "μ=2,σ=1.5"])
plt.show()
```

## 二,背景
训练深度神经网络的复杂性在于,因为前面的层的参数会发生变化导致每层输入的分布在训练过程中会发生变化。这又导致模型需要需要较低的学习率和非常谨慎的参数初始化策略,从而减慢了训练速度,并且具有饱和非线性的模型训练起来也非常困难。
网络层输入数据分布发生变化的这种现象称为**内部协变量转移**,BN 就是来解决这个问题。
### 2.1,如何理解 Internal Covariate Shift
在深度神经网络训练的过程中,由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称在论文中称之为**内部协变量偏移**(Internal Covariate Shift)。
那么,为什么网络中间层数据分布会发生变化呢?
在深度神经网络中,我们可以将每一层视为对输入的信号做了一次变换(暂时不考虑激活,因为激活函数不会改变输入数据的分布):
$$
Z = W \cdot X + B \tag{5}
$$
其中 $W$ 和 $B$ 是模型学习的参数,这个公式涵盖了全连接层和卷积层。
随着 SGD 算法更新参数,和网络的每一层的输入数据经过公式5的运算后,其 $Z$ 的**分布一直在变化**,因此网络的每一层都需要不断适应新的分布,这一过程就被叫做 Internal Covariate Shift。
而深度神经网络训练的复杂性在于每层的输入受到前面所有层的参数的影响—因此当网络变得更深时,网络参数的微小变化就会被放大。
### 2.2,Internal Covariate Shift 带来的问题
1. 网络层需要不断适应新的分布,**导致网络学习速度的降低**。
2. 网络层输入数据容易陷入到非线性的饱和状态并**减慢网络收敛**,这个影响随着网络深度的增加而放大。
随着网络层的加深,后面网络输入 $x$ 越来越大,而如果我们又采用 `Sigmoid` 型激活函数,那么每层的输入很容易移动到非线性饱和区域,此时梯度会变得很小甚至接近于 $0$,导致参数的更新速度就会减慢,进而又会放慢网络的收敛速度。
饱和问题和由此产生的梯度消失通常通过使用修正线性单元激活($ReLU(x)=max(x,0)$),更好的参数初始化方法和小的学习率来解决。然而,如果我们能保证非线性输入的分布在网络训练时保持更稳定,那么优化器将不太可能陷入饱和状态,进而训练也将加速。
### 2.3,减少 Internal Covariate Shift 的一些尝试
1. **白化(Whitening)**: 即输入线性变换为具有零均值和单位方差,并去相关。
**白化过程由于改变了网络每一层的分布**,因而改变了网络层中本身数据的表达能力。底层网络学习到的参数信息会被白化操作丢失掉,而且白化计算成本也高。
2. **标准化(normalization)**
Normalization 操作虽然缓解了 `ICS` 问题,让每一层网络的输入数据分布都变得稳定,但却导致了数据表达能力的缺失。
## 三,批量归一化(BN)
### 3.1,BN 的前向计算
论文中给出的 Batch Normalizing Transform 算法计算过程如下图所示。其中输入是一个考虑一个大小为 $m$ 的小批量数据 $\cal B$。

论文中的公式不太清晰,下面我给出更为清晰的 Batch Normalizing Transform 算法计算过程。
设 $m$ 表示 batch_size 的大小,$n$ 表示 features 数量,即样本特征值数量。在训练过程中,针对每一个 `batch` 数据,`BN` 过程进行的操作是,将这组数据 `normalization`,之后对其进行线性变换,具体算法步骤如下:
$$\begin{aligned}
\mu_B &= \frac{1}{m}\sum_1^m x_i \\
\sigma^2_B &= \frac{1}{m} \sum_1^m (x_i-\mu_B)^2 \\
n_i &= \frac{x_i-\mu_B}{\sqrt{\sigma^2_B + \epsilon}} \\
z_i &= \gamma n_i + \beta = \frac{\gamma}{\sqrt{\sigma^2_B + \epsilon}}x_i + (\beta - \frac{\gamma\mu_{B}}{\sqrt{\sigma^2_B + \epsilon}})
\end{aligned}$$
以上公式乘法都为元素乘,即 `element wise` 的乘法。其中,参数 $\gamma,\beta$ 是训练出来的, $\epsilon$ 是为零防止 $\sigma_B^2$ 为 $0$ ,加的一个很小的数值,通常为`1e-5`。公式各个符号解释如下:
| 符号 | 数据类型 | 数据形状 |
| :----------: | :------------------: | :------: |
| $X$ | 输入数据矩阵 | [m, n] |
| $x_i$ | 输入数据第 i 个样本 | [1, n] |
| $N$ | 经过归一化的数据矩阵 | [m, n] |
| $n_i$ | 经过归一化的单样本 | [1, n] |
| $\mu_B$ | 批数据均值 | [1, n] |
| $\sigma^2_B$ | 批数据方差 | [1, n] |
| $m$ | 批样本数量 | [1] |
| $\gamma$ | 线性变换参数 | [1, n] |
| $\beta$ | 线性变换参数 | [1, n] |
| $Z$ | 线性变换后的矩阵 | [1, n] |
| $z_i$ | 线性变换后的单样本 | [1, n] |
| $\delta$ | 反向传入的误差 | [m, n] |
### 3.2,BN 层如何工作
在论文中,训练一个带 `BN` 层的网络, `BN` 算法步骤如下图所示:

在训练期间,我们一次向网络提供一小批数据。在前向传播过程中,网络的每一层都处理该小批量数据。 `BN` 网络层按如下方式执行前向传播计算:

> 图片来源[这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。
注意,图中计算模型推理时 BN 层均值与方差的无偏估计方法,是吴恩达在 Coursera 上的 Deep Learning 课程上提出的方法:对 train 阶段每个 batch 计算的 mean/variance 采用**指数加权平均**来得到 test 阶段 mean/variance 的估计。
在训练期间,它只是计算此 EMA,但不对其执行任何操作。在训练结束时,它只是将该值保存为层状态的一部分,以供在推理阶段使用。
如下图可以展示BN 层的前向传播计算过程数据的 `shape` ,红色框出来的单个样本都指代单个矩阵,即运算都是在单个矩阵运算中计算的。

> 图片来源 [这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。
BN 的反向传播过程中,会更新 BN 层中的所有 $\beta$ 和 $\gamma$ 参数。
### 3.3,推理时的 BN 层
批量归一化(batch normalization)的“批量”两个字,表示在模型的迭代训练过程中,BN 首先计算小批量( mini-batch,如 32)的均值和方差。但是,在推理过程中,我们只有一个样本,而不是一个小批量。即在模型训练的时候,均值 $\mu$、方差 $\sigma^2$、$\gamma$、$\beta$ 是一直在更新的,但是,在推理的时候,这四个值都是固定了的。
虽然 $\gamma$、$\beta$ 参数可通过模型训练后得到,但是,有一个问题,模型推理时 BN 层的均值和方差应该如何获得呢?
第一种方法是,使用的均值和方差数据是在训练过程中样本值的平均,即:
$$\begin{align}
E[x] &= E[\mu_B] \nonumber \\
Var[x] &= \frac{m}{m-1} E[\sigma^2_B] \nonumber \\
\end{align}$$
这种做法会把所有训练批次的 $\mu$ 和 $\sigma$ 都保存下来,然后在最后训练完成时(或做测试时)做下平均。
第二种方法是使用类似动量的方法,训练时,加权平均每个批次的值,权值 $\alpha$ 可以为0.9:
$$\begin{aligned}
\mu_{mov_{i}} &= \alpha \cdot \mu_{mov_{i}} + (1-\alpha) \cdot \mu_i \nonumber \\
\sigma_{mov_{i}} &= \alpha \cdot \sigma_{mov_{i}} + (1-\alpha) \cdot \sigma_i \nonumber \\
\end{aligned}$$
模型推理或测试时,直接使用模型文件中保存的 $\mu_{mov_{i}}$ 和 $\sigma_{mov_{i}}$ 的值即可。
同时,上面 `BN` 的计算公式(算法 1)可以变形为:
$$
z_i = \gamma n_i + \beta = \frac{\gamma}{\sqrt{\sigma^2_B + \epsilon}}x_i + (\beta - \frac{\gamma\mu_{B}}{\sqrt{\sigma^2_B + \epsilon}}) = ax_i + b\nonumber
$$
因为推理时,均值 $\mu$、方差 $\sigma^2$、$\gamma$、$\beta$ 值固定,因此可以看出推理时 `BN` 本质上是做线性变换,且 $a$、$b$ 都是常数。
### 3.4,实验
`BN` 在 `ImageNet` 分类数据集上实验结果是 `SOTA` 的,如下表所示:

### 3.5,BN 层的作用
通过对神经网络中每一层的输入数据进行归一化处理,**使得每一层的输入分布更加稳定**,从而降低了神经网络过拟合的风险。具体而言,BN 层的作用包括:
1. **BN 使得网络中每层输入数据的分布相对稳定,加速模型训练和收敛速度**。
2. **批标准化可以提高学习率**。在传统的深度网络中,学习率过高可能会导致梯度爆炸或梯度消失,以及陷入差的局部最小值。批标准化有助于解决这些问题。通过标准化整个网络的激活值,它可以防止层参数的微小变化随着数据在深度网络中的传播而放大。例如,这使 sigmoid 非线性更容易保持在它们的非饱和状态,这对训练深度 sigmoid 网络至关重要,但在传统上很难实现。
3. **BN 允许网络使用饱和非线性激活函数(如 sigmoid,tanh 等)进行训练,其能缓解梯度消失问题**。如果不使用 BN,由于深度神经网络的“累积”效应,使得低层网络的变化效应会累加到高层网络,导致模型训练过程很容易进入到 sigmoid 激活函数的梯度饱和区;而经过 BN 层操作后可以让激活函数的输入数据落在梯度非饱和区,缓解梯度消失的问题。(bn 层一般在激活函数层前面)
4. BN 层能够**减少过拟合和提高模型的泛化能力**,不再需要 `dropout` 和 `LRN`(Local Response Normalization)层来实现正则化。BN 层将每个特征在当前的 mini-batch 中进行标准化,其中包括均值和方差的估计。由于mini-batch的选择是随机的,所以在每个 mini-batch 中使用的均值和方差的估计也是随机的,这样就引入了一定的噪声。
5. **减少对参数初始化方法的依赖**。早期的权重是随机初始化的,异常权重值会扭曲梯度,导致网络收敛需要更长的时间,而 `Batch Norm` 有助于抑制这些权重异常值的影响。
Sigmoid 导数的**梯度消失区域**如下图所示:

## 参考资料
1. [Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift](https://arxiv.org/abs/1502.03167)
2. [维基百科-正态分布](https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E6%80%81%E5%88%86%E5%B8%83)
3. [Batch Norm Explained Visually — How it works, and why neural networks need it](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)
4. [15.5 批量归一化的原理](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC7%E6%AD%A5%20-%20%E6%B7%B1%E5%BA%A6%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/15.5-%E6%89%B9%E9%87%8F%E5%BD%92%E4%B8%80%E5%8C%96%E7%9A%84%E5%8E%9F%E7%90%86.html)
5. [Batch Normalization原理与实战](https://zhuanlan.zhihu.com/p/34879333)
================================================
FILE: 2-deep_learning_basic/cnn基础部件-卷积层详解.md
================================================
---
layout: post
title: cnn 基础部件-卷积层详解
date: 2022-12-15 22:00:00
summary: 卷积神经网络核心网络层是卷积层,其使用了卷积(convolution)这种数学运算,卷积是一种特殊的线性运算。
categories: DeepLearning
---
- [前言](#前言)
- [一,卷积](#一卷积)
- [1.1,卷积运算定义](#11卷积运算定义)
- [1.2,卷积的意义](#12卷积的意义)
- [1.3,从实例理解卷积](#13从实例理解卷积)
- [1.4,图像卷积(二维卷积)](#14图像卷积二维卷积)
- [1.5,互相关和卷积](#15互相关和卷积)
- [二,卷积层](#二卷积层)
- [2.1,卷积层定义](#21卷积层定义)
- [2.1.1,局部连接](#211局部连接)
- [2.1.2,权重共享](#212权重共享)
- [2.2,卷积层理解](#22卷积层理解)
- [2.3,分组卷积和DW卷积](#23分组卷积和dw卷积)
- [2.4,卷积层 api](#24卷积层-api)
- [4.5,卷积层代码简单实现](#45卷积层代码简单实现)
- [三,卷积神经网络](#三卷积神经网络)
- [3.1,汇聚层](#31汇聚层)
- [3.2.,汇聚层 api](#32汇聚层-api)
- [四,卷积神经网络结构](#四卷积神经网络结构)
- [参考资料](#参考资料)
## 前言
在全连接层构成的多层感知机网络中,我们通过将图像数据展平成一维向量来送入模型,这样会忽略了每个图像的**空间结构信息**。理想的策略应该是要利用相近像素之间的相互关联性,将图像数据二维矩阵送给模型中学习。
卷积神经网络(convolutional neural network,`CNN`)正是一类强大的、专为处理图像数据(多维矩阵)而设计的神经网络。在 `Transformer` 应用到 `CV` 领域之前,基于卷积神经网络架构的模型在计算机视觉领域中占主导地位,几乎所有的图像识别、目标检测、语义分割、3D目标检测、视频理解等任务都是以 `CNN` 方法为基础。
卷积神经网络核心网络层是卷积层,其使用了卷积(convolution)这种数学运算,卷积是一种特殊的线性运算。另外,通常来说,卷积神经网络中用到的卷积运算和其他领域(例如工程领域以及纯数学领域)中的定义并不完全一致。
## 一,卷积
在理解卷积层之前,我们首先得理解什么是卷积操作。
卷积与[傅里叶变换](https://zh.wikipedia.org/wiki/傅里叶变换)有着密切的关系。例如两函数的傅里叶变换的乘积等于它们卷积后的傅里叶变换,利用此一性质,能简化傅里叶分析中的许多问题。
> operation 视语境有时译作“操作”,有时译作“运算”,本文不做区分。
### 1.1,卷积运算定义
为了给出卷积的定义, 这里从现实世界会用到函数的例子出发。
假设我们正在用激光传感器追踪一艘宇宙飞船的位置。我们的激光传感器给出 一个单独的输出 $x(t)$,表示宇宙飞船在时刻 $t$ 的位置。$x$ 和 $t$ 都是**实值**的,这意味着我们可以在任意时刻从传感器中读出飞船的位置。
现在假设我们的传感器受到一定程度的噪声干扰。为了得到飞船位置的低噪声估计,我们对得到的测量结果进行平均。显然,时间上越近的测量结果越相关,所 以我们采用一种**加权平均**的方法,对于最近的测量结果赋予更高的权重。我们可以采用一个加权函数 $w(a)$ 来实现,其中 $a$ 表示测量结果距当前时刻的时间间隔。如果我们对任意时刻都采用这种加权平均的操作,就得到了一个新的对于飞船位置的平滑估计函数 $s$:
$$s(t) = \int x(a)w(t-a )da$$
这种运算就叫做卷积(`convolution`)。更一般的,卷积运算的数学公式定义如下:
$$
连续定义: \; h(x)=(f*g)(x) = \int_{-\infty}^{\infty} f(t)g(x-t)dt \tag{1}
$$
$$
离散定义: \; h(x) = (f*g)(x) = \sum^{\infty}_{t=-\infty} f(t)g(x-t) \tag{2}
$$
以上卷积计算公式可以这样理解:
1. 先对函数 $g(t)$ 进行反转(`reverse`),相当于在数轴上把 $g(t)$ 函数从右边褶到左边去,也就是卷积的“卷”的由来。
2. 然后再把 $g(t)$ 函数向左平移 $x$ 个单位,在这个位置对两个函数的对应点相乘,然后相加,这个过程是卷积的“积”的过程。
### 1.2,卷积的意义
对卷积这个名词,可以这样理解:**所谓两个函数的卷积($f*g$),本质上就是先将一个函数翻转,然后进行滑动叠加**。在连续情况下,叠加指的是对两个函数的乘积求积分,在离散情况下就是加权求和,为简单起见就统一称为叠加。
因此,卷积运算整体来看就是这么一个过程:
翻转—>滑动—>叠加—>滑动—>叠加—>滑动—>叠加.....
多次滑动得到的一系列叠加值,构成了卷积函数。
> 这里多次滑动过程对应的是 $t$ 的变化过程。
那么,**卷积的意义是什么呢**?可以从卷积的典型应用场景-图像处理来理解:
1. 为什么要进行“卷”?进行“卷”(翻转)的目的其实是施加一种约束,它指定了在“积”的时候以什么为参照。在空间分析的场景,它指定了在哪个位置的周边进行累积处理。
2. 在图像处理的中,卷积处理的结果,其实就是把每个像素周边的,甚至是整个图像的像素都考虑进来,对当前像素进行某种加权处理。因此,“积”是全局概念,或者说是一种“混合”,把两个函数进行时间(信号分析)或空间(图像处理)上进行混合。
> 卷积意义的理解来自[知乎问答](https://www.zhihu.com/question/22298352),有所删减和优化。
### 1.3,从实例理解卷积
一维卷积的实例有 “丢骰子” 等经典实例,这里不做展开描述,本文从二维卷积用于图像处理的实例来理解。
一般,数字图像可以表示为如下所示矩阵:
> 本图片摘自[知乎用户马同学的文章](https://www.zhihu.com/question/22298352)。

而卷积核 $g$ 也可以用一个矩阵来表示,如:
$$
g = \begin{bmatrix}
&b_{-1,-1} &b_{-1,0} &b_{-1,1} \\
&b_{0,-1} &b_{0,0} &b_{0,1} \\
&b_{1,-1} &b_{1,0} &b_{1,1}
\end{bmatrix}
$$
按照卷积公式的定义,则目标图片的第 $(u, v)$ 个像素的二维卷积值为:
$$
(f * g)(u, v)=\sum_{i} \sum_{j} f(i, j)g(u-i, v-j)=\sum_{i} \sum_{j} a_{i,j} b_{u-i,v-j}
$$
展开来分析二维卷积计算过程就是,首先得到原始图像矩阵中 $(u, v)$ 处的矩阵:
$$
f=\begin{bmatrix}
&a_{u-1,v-1} &a_{u-1,v} &a_{u-1,v+1}\\
&a_{u,v-1} &a_{u,v} &a_{u,v+1} \\
&a_{u+1,v-1} &a_{u+1,v} &a_{u+1,v+1}
\end{bmatrix}
$$
然后将图像处理矩阵**翻转**(两种方法,结果等效),如先沿 $x$ 轴翻转,再沿 $y$ 轴翻转(**相当于将矩阵 $g$ 旋转 180 度**):
$$
\begin{aligned}
g &= \begin{bmatrix} &b_{-1,-1} &b_{-1,0} &b_{-1,1}\\ &b_{0,-1} &b_{0,0} &b_{0,1} \\ &b_{1,-1} &b_{1,0} &b_{1,1} \end{bmatrix}
=> \begin{bmatrix} &b_{1,-1} &b_{1,0} &b_{1,1}\\ &b_{0,-1} &b_{0,0} &b_{0,1} \\ &b_{-1,-1} &b_{-1,0} &b_{-1,1} \end{bmatrix} \\
&= \begin{bmatrix} &b_{1,1} &b_{1,0} &b_{1,-1}\\ &b_{0,1} &b_{0,0} &b_{0,-1} \\ &b_{-1,1} &b_{-1,0} &b_{-1,-1} \end{bmatrix} = g^{'}
\end{aligned}
$$
最后,计算卷积时,就可以用 $f$ 和 $g′$ 的内积:
$$
\begin{aligned}
f * g(u,v) &= a_{u-1,v-1} \times b_{1,1} + a_{u-1,v} \times b_{1,0} + a_{u-1,v+1} \times b_{1,-1} \\
&+ a_{u,v-1} \times b_{0,1} + a_{u,v} \times b_{0,0} + a_{u,v+1} \times b_{0,-1} \\
&+ a_{u+1,v-1} \times b_{-1,1} + a_{u+1,v} \times b_{-1,0} + a_{u+1,v+1} \times b_{-1,-1}
\end{aligned}
$$
计算过程可视化如下动图所示,注意动图给出的是 $g$ 不是 $g'$。

以上公式有一个特点,做乘法的两个对应变量 $a, b$ 的下标之和都是 $(u,v)$,其目的是对这种加权求和进行一种约束,这也是要将矩阵 $g$ 进行翻转的原因。上述计算比较麻烦,实际计算的时候,都是用翻转以后的矩阵,直接求**矩阵内积**就可以了。
### 1.4,图像卷积(二维卷积)
在机器学习和图像处理领域,卷积的主要功能是**在一个图像(或某种特征) 上滑动一个卷积核(即滤波器),通过卷积操作得到一组新的特征**。一幅图像在经过卷积操作后得到结果称为特征映射(`Feature Map`)。如果把图像矩阵简写为 $I$,把卷积核 `Kernal` 简写为 $K$,则目标图片的第 $(i,j)$ 个像素的卷积值为:
$$
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(m,n)K(i-m,j-n) \tag{3}
$$
可以看出,这和一维情况下的卷积公式 2 是一致的。因为卷积的可交换性,我们也可以把公式 3 等价地写作:
$$
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(i-m,j-n)K(m,n) \tag{4}
$$
通常,下面的公式在机器学习库中实现更为简单,因为 $m$ 和 $n$ 的有效取值范围相对较小。
卷积运算可交换性的出现是因为我们将核相对输入进行了翻转(`flip`),从 $m$ 增 大的角度来看,输入的索引在增大,但是卷积核的索引在减小。我们将**卷积核翻转的唯一目 的是实现可交换性**。尽管可交换性在证明时很有用,但在神经网络的应用中却不是一个重要的性质。相反,许多神经网络库会实现一个**互相关函数**(`corresponding function`),它与卷积相同但没有翻转核:
$$
h(i,j) = (I*K)(i,j)=\sum_m \sum_n I(i+m,j+n)K(m,n) \tag{5}
$$
互相关函数的运算,是两个序列滑动相乘,两个序列都不翻转。卷积运算也是滑动相乘,但是其中一个序列需要先翻转,再相乘。
### 1.5,互相关和卷积
互相关和卷积运算的关系,可以通过下述公式理解:
$$
\begin{aligned}Y
&= W\otimes X \\
&= \text{rot180}(W) * X \\
\end{aligned}
$$
其中 $\otimes$ 表示互相关运算,$*$ 表示卷积运算,$\text{rot180(⋅)}$ 表示旋转 `180` 度,$Y$ 为输出矩阵。从上式可以看出,互相关和卷积的区别仅仅在于卷积核是否进行翻转。因此互相关也可以称为不翻转卷积.
> 离散卷积可以看作矩阵的乘法,然而,这个矩阵的一些元素被限制为必须和另外一些元素相等。
在神经网络中使用卷积是为了进行特征抽取,**卷积核是否进行翻转和其特征抽取的能力无关**(特别是当卷积核是可学习的参数时),**因此卷积和互相关在能力上是等价的**。事实上,很多深度学习工具中卷积操作其实都是互相关操作,用来**减少一些不必要的操作或开销(不反转 Kernal)**。
总的来说,
1. 我们实现的卷积操作不是原始数学含义的卷积,而是工程上的卷积,但一般也简称为卷积。
2. 在实现卷积操作时,并不会反转卷积核。
## 二,卷积层
> 在传统图像处理中,**线性空间滤波**的原理实质上是指指图像 $f$ 与滤波器核 $w$ 进行乘积之和(**卷积**)运算。核是一个矩阵,其大小定义了运算的邻域,其系数决定了该滤波器(也称模板、窗口滤波器)的性质,并通过设计不同核系数(卷积核)来实现低通滤波(平滑)和高通滤波(锐化)功能,因此我们可以认为卷积是利用某些设计好的参数组合(卷积核)去提取图像空域上相邻的信息。
### 2.1,卷积层定义
在全连接前馈神经网络中,如果第 $l$ 层有 $M_l$ 个神经元,第 $l-1$ 层有 $M_{l-1}$ 个 神经元,连接边有 $M_{l}\times M_{l-1}$ 个,也就是权重矩阵有 $M_{l}\times M_{l-1}$ 个参数。当 $M_l$ 和 $M_{l-1}$ 都很大时,权重矩阵的参数就会非常多,训练的效率也会非常低。
如果采用卷积来代替全连接,第 $l$ 层的净输入 $z^{(l)}$ 为第 $l-1$ 层激活值 $a^{(l−1)}$ 和滤波器 $w^{(l)}\in \mathbb{R}^K$ 的卷积,即
$$
z^{(l)} = w^{(l)}\otimes a^{(l−1)} + b^{(l)}
$$
其中 $b^{(l)}\in \mathbb{R}$ 为可学习的偏置。
> 上述卷积层公式也可以写成这样的形式:$Z = W*A+b$
根据卷积层的定义,卷积层有两个很重要的性质: 局部连接和权重共享。
#### 2.1.1,局部连接
**局部连接**:在卷积层(假设是第 $l$ 层)中的每一个神经元都只和下一层(第 $l + 1$ 层)中某个局部窗口内的神经元相连,构成一个局部连接网络。其实可能表达为**稀疏交互**更直观点,传统的网络层是全连接的,使用矩阵乘法来建立输入与输出的连接关系。矩阵的每个参数都是独立的,它描述了每个输入单元与输出单元的交互。这意味着每个输出单元与所有的输入单元都产生关联。而卷积层通过使用**卷积核矩阵**来实现稀疏交互(也称作稀疏连接,或者稀疏权重),每个输出单元仅仅与特定的输入神经元(其实是指定通道的 `feature map`)产生关联。
下图显示了全连接层和卷积层的每个输入单元影响的输出单元比较:

- 对于传统全连接层,每个输入单元影响了所有的输出单元。
- 对于卷积层,每个输入单元只影响了3个输出单元(核尺寸为3时)。
#### 2.1.2,权重共享
**权重共享**:卷积层中,同一个核会在输入的不同区域做卷积运算。全连接层和卷积层的权重参数比较如下图:

- 对于传统全连接层: $x_3\to s_3$ 的权重 $w_{3,3}$ 只使用了一次 。
- 对于卷积层: $x_3\to s_3$ 的权重 $w_{3,3}$ 被共享到 $x_i \to s_i, i = 1,2,4,5$。
全连接层和卷积层的可视化对比如下图所示:

总结:一个滤波器(3维卷积核)只捕捉输入数据中的一种特定的局部特征。为了提高卷积网络的表示能力,可以在每一层使用多个不同的特征映射,即增加滤波器的数量,以更好地提取图像的特征。
### 2.2,卷积层理解
前面章节内容中,卷积的输出形状只取决于输入形状和卷积核的形状。而神经网络中的卷积层,在卷积的标准定义基础上,还引入了卷积核的滑动步长和零填充来增加卷积的多样性,从而可以更灵活地进行特征抽取。
- 步长(Stride):指卷积核每次滑动的距离
- 零填充(Zero Padding):在输入图像的边界填充元素(通常填充元素是0)
卷积层定义:每个卷积核(`Kernel`)在输入矩阵上滑动,并通过下述过程实现卷积计算:
1. 在来自卷积核的元素和输入特征图子矩阵的元素之间进行乘法以获得输出感受野。
2. 然后将相乘的值与添加的偏差相加以获得输出矩阵中的值。
通道数为 1 的输入特征图应用卷积层的数值计算过程可视化如下图 1 所示:

> 图片来源论文 [Improvement of Damage Segmentation Based on Pixel-Level Data Balance Using VGG-Unet](https://www.researchgate.net/figure/Example-of-Convolutional-layer_fig2_348296106)。
注意,卷积层的输出 `Feature map` 的大小取决于输入的大小、Pad 数、卷积核大小和步长。在 `Pytorch` 框架中,图片(`feature map`)经卷积 `Conv2D` 后**输出大小计算公式**如下:$\left \lfloor N = \frac{W-F+2P}{S}+1 \right \rfloor$。
其中 $\lfloor \rfloor$ 是向下取整符号,用于结果不是整数时进行向下取整(`Pytorch` 的 `Conv2d` 卷积函数的默认参数 `ceil_mode = False`,即默认向下取整, `dilation = 1`),其他符号解释如下:
+ 输入图片大小 `W×W`(默认输入尺寸为正方形)
+ `Filter` 大小 `F×F`
+ 步长 `S`
+ padding的像素数 `P`
+ 输出特征图大小 `N×N`
上图1侧重于解释数值计算过程,而下图2则侧重于解释卷积层的五个核心概念的关系:
- 输入 Input Channel
- 卷积核组 WeightsBias
- 过滤器 Filter
- 卷积核 kernal
- 输出 Feature Map

上图是三通道经过两组过滤器的卷积过程,在这个例子中,输入是三维数据 $3\times 32 \times32$,经过权重参数尺寸为 $2\times 3\times 5\times 5$ 的卷积层后,输出为三维 $2\times 28\times 28$,维数并没有变化,只是每一维内部的尺寸有了变化,一般都是要向更小的尺寸变化,以便于简化计算。
假设三维卷积核(也叫滤波器)尺寸为 $(c_{in}, k, k)$,一共有 $c_{out}$ 个滤波器,即卷积层参数尺寸为 $(c_{out}, c_{in}, k, k)$ ,则标准卷积层计算有以下**特点**:
1. 输出的 `feature map` 的数量等于滤波器数量 $c_{out}$,即卷积层参数值确定后,feature map 的数量也确定,而不是根据前向计算自动计算出来;
2. 对于每个输出,都有一个对应的过滤器 Filter,图例中 Feature Map-1 对应 Filter-1;
3. **每个 Filter 内都有一个或多个卷积核 Kernal,对应每个输入通道(Input Channel)**,图例为 3,对应输入的红绿蓝三个通道;
4. 每个 Filter 只有一个 Bias 值,图例中 Filter-1 对应 b1;
5. 卷积核 Kernal 的大小一般是奇数如:$1\times 1$,$3\times 3$。
**注意**,以上内容都描述的是**标准卷积**,随着技术的发展,后续陆续提出了分组卷积、深度可分离卷积、空洞卷积等。详情可参考我之前的文章-[MobileNetv1论文详解](https://github.com/HarleysZhang/cv_note/blob/master/7-model_compression/%E8%BD%BB%E9%87%8F%E7%BA%A7%E7%BD%91%E7%BB%9C%E8%AE%BA%E6%96%87%E8%A7%A3%E6%9E%90/MobileNetv1%E8%AE%BA%E6%96%87%E8%AF%A6%E8%A7%A3.md)。
### 2.3,分组卷积和DW卷积
和标准卷积每个 Filter 内都有一个或多个卷积核 Kernal,对应每个输入通道(Input Channel)的特性不同,分组卷积和 DW 卷积的特点如下:
- 分组卷积:**分组卷积是将输入通道分成若干组**,**每组的滤波器只与其同组的输入 feature map 进行卷积**,最终将每组的输出通道拼接在一起得到最终输出。
- DW 卷积:每个 Filter 内只有一个卷积核 Kernal,对应每个输入通道(Input Channel),即对于每个输入通道分别使用一个固定大小的卷积核进行卷积操作。
分组卷积的极致是分组数数等于输入通道数,这其实就是 `DW` 卷积,可视化如下:

另外,对于 `pytorch` 的卷积层 api 是同时支持普通卷积、分组卷积/DW 卷积的。但值得注意的是,对于分组卷积,卷积层的输出通道数必须是分组数的整数倍,否则代码会报错!
```python
import torch
input = torch.randn([20, 10, 224, 224]) # input_channels = 10
conv3x3 = torch.nn.Conv2d(in_channels = 10, output_channels = 5, kernel_size=3, groups=5)
output = conv3x3(input)
print(conv3x3.weight.shape)
print(output.shape)
```
如果将 `groups=5` 改为 `groups=6`或者将 `output_channels = 5` 改为 `6`,则会报错:
```bash
ValueError: in_channels must be divisible by groups
ValueError: out_channels must be divisible by groups
```
### 2.4,卷积层 api
注意,`2D` 卷积的卷积核权重是一个 `4D` 张量,包含输出通道,输入通道,高,宽。对于 `Pytorch/Caffe` 深度学习框架,其输入输出数据的尺寸都是 (`(N, C, H, W)`),卷积核权重 `shape` 如下:
- 常规卷积的卷积核权重 `shape`:(`C_out, C_in, kernel_height, kernel_width`)
- 分组卷积的卷积核权重 `shape`:(`C_out, C_in/g, kernel_height, kernel_width`)
- `DW` 卷积的卷积核权重`shape`:(`C_in, 1, kernel_height, kernel_width`)
`Pytorch` 框架中对应的 2D 卷积层 api 如下:
```python
# 对应常规卷积的卷积核权重 shape 都为(out_channels, in_channels, kernel_height, kernel_width)
class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
```
**主要参数解释:**
+ `in_channels`(`int`) – 输入信号的通道。
+ `out_channels`(`int`) – 卷积产生的通道。
+ `kerner_size`(`int or tuple`) - 卷积核的尺寸。
+ `stride`(`int or tuple`, `optional`) - 卷积步长,默认值为 `1` 。
+ `padding`(`int or tuple`, `optional`) - 输入的每一条边补充 `0` 的层数,默认不填充。
+ `dilation`(`int or tuple`, `optional`) – 卷积核元素之间的间距,默认取值 `1` 。
+ `groups`(`int`, `optional`) – 从输入通道到输出通道的阻塞连接数。
+ `bias`(`bool`, `optional`) - 如果 `bias=True`,添加偏置。
**示例代码:**
```python
###### Pytorch卷积层输出大小验证
import torch
import torch.nn as nn
import torch.autograd as autograd
# With square kernels and equal stride
# output_shape: height = (50-3)/2+1 = 24.5,卷积向下取整,所以 height=24.
m = nn.Conv2d(16, 33, 3, stride=2)
# # non-square kernels and unequal stride and with padding
# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2)) # 输出shape: torch.Size([20, 33, 28, 100])
# # non-square kernels and unequal stride and with padding and dilation
# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1)) # 输出shape: torch.Size([20, 33, 26, 100])
input = autograd.Variable(torch.randn(20, 16, 50, 100))
output = m(input)
print(output.shape) # 输出shape: torch.Size([20, 16, 24, 49])
```
### 4.5,卷积层代码简单实现
**卷积层参数**和全连接参数是类似的,都是权重 `weights` 和偏移向量 `bias` 的组合,但是区别在于卷积层的权重矩阵是四维的(`conv2d`),而全连接层的权重二维的,但偏移向量都是列向量。
基于 `numpy` 库,没有经过优化版本的卷积层代码实现如下:
```python
for bs in range(batch_size):
for oc in range(output_channels):
for oh in range(output_height):
for ow in range(output_weight):
# input 三维矩阵 kernel 三维矩阵相乘, 默认 array1*array2 就是对应元素的乘积
output[bs, oc, oh, ow] = np.sum(input[bs, :, oh: oh+kernel_size, ow: ow+kernel_size] * weights[oc, :, :, :]) + bias[oc]
return output
```
如果不用 `numpy` 函数,for 循环的次数会更多,实现方式如下:
```python
stride = 1
kernel_size = 3
for bs in range(batch_size):
for oc in range(output_channels):
output[bs, oc, oh, ow] += bias[oc]
for ic in range(input_channels):
for oh in range(height):
for ow in range(width):
for kh in range(kernel_size):
for kw in range(kernel_size):
output[bs, oc, oh, ow] += input[bs, ic, oh+kh, ow+kw] * weights[oc, ic, kh, kw]
```
## 三,卷积神经网络
卷积神经网络一般由卷积层、汇聚层和全连接层构成。
### 3.1,汇聚层
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。
汇聚层(Pooling Layer)也叫子采样层(Subsampling Layer),其作用不仅是进降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。
与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。然而,不同于卷积层中的输入与卷积核之间的互相关计算,**汇聚层不包含参数**。相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为**最大汇聚层**(maximum pooling)和**平均汇聚层**(average pooling)。通道数为 1 的输入特征图应用最大汇聚层计算过程的可视化如下所示:

在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上⻆开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。
比如最大汇聚层,其计算输入中区域的最大值。

值得注意的是,与卷积层一样,汇聚层也可以通过改变填充和步幅以获得所需的输出形状。
### 3.2.,汇聚层 api
`Pytorch` 框架中对应的聚合层 api 如下:
```python
class torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)
```
**主要参数解释**:
+ `kernel_size`(`int or tuple`):`max pooling` 的窗口大小。
+ `stride`(`int or tuple`, `optional):`max pooling` 的窗口移动的步长。默认值是 `kernel_size`。
+ `padding`(`int or tuple`, `optional`):**默认值为 `0`,即不填充像素**。输入的每一条边补充 `0` 的层数。
+ `dilation`:滑动窗中各元素之间的距离。
+ `ceil_mode`:默认值为 `False`,即上述公式默认向下取整,如果设为 `True`,计算输出信号大小的时候,公式会使用向上取整。
> `Pytorch` 中池化层默认`ceil mode = false`,而 `Caffe` 只实现了 `ceil mode= true` 的计算方式。
**示例代码:**
```python
import torch
import torch.nn as nn
import torch.autograd as autograd
# 大小为3,步幅为2的正方形窗口池
m = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# pool of non-square window
input = autograd.Variable(torch.randn(20, 16, 50, 32))
output = m(input)
print(output.shape) # torch.Size([20, 16, 25, 16])
```
## 四,卷积神经网络结构
一个典型的卷积网络结构是由卷积层、汇聚层、全连接层交叉堆叠而成。如下图所示:

一个简单的 `CNN` 网络连接图如下所示。
> 经典 `CNN` 网络的总结,可参考我之前写的文章-[经典 backbone 网络总结](https://github.com/HarleysZhang/cv_note/blob/master/5-deep_learning/%E7%BB%8F%E5%85%B8backbone%E8%AF%A6%E8%A7%A3/%E7%BB%8F%E5%85%B8backbone%E6%80%BB%E7%BB%93.md)。

目前,卷积网络的整体结构趋向于使用更小的卷积核(比如 $1 \times 1$ 和 $3 \times 3$) 以及更深的结构(比如层数大于 50)。另外,由于卷积层的操作性越来越灵活(同样可完成减少特征图分辨率),汇聚层的作用越来越小,因此目前的卷积神经网络逐渐趋向于全卷积网络。
另外,可通过这个[网站](https://poloclub.github.io/cnn-explainer/#article-convolution)可视化 `cnn` 的全部过程。

## 参考资料
1. [AI EDU-17.3 卷积的反向传播原理](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC8%E6%AD%A5%20-%20%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/17.3-%E5%8D%B7%E7%A7%AF%E7%9A%84%E5%8F%8D%E5%90%91%E4%BC%A0%E6%92%AD%E5%8E%9F%E7%90%86.html)
2. [Visualizing the Feature Maps and Filters by Convolutional Neural Networks](https://medium.com/dataseries/visualizing-the-feature-maps-and-filters-by-convolutional-neural-networks-e1462340518e)
3. 《神经网络与深度学习》-第5章
4. 《动手学深度学习》-第6章
5. https://www.zhihu.com/question/22298352
6. [卷积神经网络](https://www.huaxiaozhuan.com/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/chapters/5_CNN.html)
================================================
FILE: 2-deep_learning_basic/cnn基础部件-激活函数详解.md
================================================
---
layout: post
title: cnn 基础部件-激活函数详解
date: 2022-12-05 22:00:00
summary: 本文分析了激活函数对于神经网络的必要性,同时讲解了几种常见的激活函数的原理,并给出相关公式、代码和示例图。
categories: DeepLearning
---
- [一 激活函数概述](#一-激活函数概述)
- [1.1 前言](#11-前言)
- [1.2 激活函数定义](#12-激活函数定义)
- [1.3 激活函数性质](#13-激活函数性质)
- [二 Sigmoid 型函数(挤压型激活函数)](#二-sigmoid-型函数挤压型激活函数)
- [2.1 Logistic(sigmoid)函数](#21-logisticsigmoid函数)
- [2.2 Tanh 函数](#22-tanh-函数)
- [三 ReLU 函数及其变体(半线性激活函数)](#三-relu-函数及其变体半线性激活函数)
- [3.1 ReLU 函数](#31-relu-函数)
- [3.2,Leaky ReLU/PReLU/ELU/Softplus 函数](#32leaky-relupreluelusoftplus-函数)
- [四 Swish 函数](#四-swish-函数)
- [五 激活函数总结](#五-激活函数总结)
- [参考资料](#参考资料)
> 本文分析了激活函数对于神经网络的必要性,同时讲解了几种常见的激活函数的原理,并给出相关公式、代码和示例图。
## 一 激活函数概述
### 1.1 前言
人工神经元(Artificial Neuron),简称神经元(Neuron),是构成神经网络的基本单元,其主要是模拟生物神经元的结构和特性,接收一组输入信号并产生输出。生物神经元与人工神经元的对比图如下所示。
<div align="center">
<img src="../images/activation_function/neuron.png" width="60%" alt="neuron">
</div>
从机器学习的角度来看,神经网络其实就是一个**非线性模型**,其基本组成单元为具有非线性激活函数的神经元,通过大量神经元之间的连接,使得多层神经网络成为一种高度非线性的模型。**神经元之间的连接权重就是需要学习的参数**,其可以在机器学习的框架下通过**梯度下降方法**来进行学习。
> 深度学习一般指的是深度神经网络模型,泛指网络层数在三层或者三层以上的神经网络结构。
### 1.2 激活函数定义
激活函数(也称“非线性映射函数”),是深度卷积神经网络模型中必不可少的网络层。
假设一个神经元接收 $D$ 个输入 $x_1, x_2,⋯, x_D$,令向量 $x = [x_1;x_2;⋯;x_𝐷]$ 来表示这组输入,并用净输入(Net Input) $z \in \mathbb{R}$ 表示一个神经元所获得的输入信号 $x$ 的加权和:
$$
z = \sum_{d=1}^{D} w_{d}x_{d} + b = w^\top x + b
$$
其中 $w = [w_1;w_2;⋯;w_𝐷]\in \mathbb{R}^D$ 是 $D$ 维的权重矩阵,$b \in \mathbb{R}$ 是偏置向量。
以上公式其实就是**带有偏置项的线性变换**(类似于放射变换),本质上还是属于线形模型。为了转换成非线性模型,我们在净输入 $z$ 后添加一个**非线性函数** $f$(即激活函数)。
$$a = f(z)$$
由此,典型的神经元结构如下所示:
<div align="center">
<img src="../images/activation_function/typical_neuron_architecture.png" width="60%" alt="典型的神经元架构">
</div>
### 1.3 激活函数性质
为了增强网络的表示能力和学习能力,激活函数需要具备以下几点性质:
1. **连续并可导(允许少数点上不可导)的非线性函数**。可导的激活函数 可以直接利用数值优化的方法来学习网络参数。
2. 激活函数及其导函数要**尽可能的简单**,有利于提高网络计算效率。
3. 激活函数的导函数的**值域要在一个合适的区间内**,不能太大也不能太小,否则会影响训练的效率和稳定性.
## 二 Sigmoid 型函数(挤压型激活函数)
Sigmoid 型函数是指一类 S 型曲线函数,为两端饱和函数。常用的 Sigmoid 型函数有 Logistic 函数和 Tanh 函数。
> 相关数学知识: 对于函数 $f(x)$,若 $x \to −\infty$ 时,其导数 ${f}'\to 0$,则称其为左饱和。若 $x \to +\infty$ 时,其导数 ${f}'\to 0$,则称其为右饱和。当同时满足左、右饱和时,就称为两端饱和。
### 2.1 Logistic(sigmoid)函数
对于一个定义域在 $\mathbb{R}$ 中的输入,`sigmoid` 函数将输入变换为区间 `(0, 1)` 上的输出。因此,sigmoid 通常称为**挤压函数**(squashing function): 它将范围 (-inf, inf) 中的任意输入压缩到区间 (0, 1) 中的某个值:
$$
\sigma(x) = \frac{1}{1 + exp(-x)}
$$
sigmoid 函数常记作 $\sigma(x)$。它的导数公式如下所示:
$$
\frac{\mathrm{d} }{\mathrm{d} x}\text{sigmoid}(x) = \frac{exp(-x)}{(1+exp(-x))^2} = \text{sigmoid}(x)(1 - \text{sigmoid}(x))
$$
sigmoid 函数及其导数曲线如下所示:
<div align="center">
<img src="../images/activation_function/sigmoid_and_gradient_curve2.png" width="70%" alt="sigmoid 函数及其导数图像">
</div>
可以看出,sigmoid 函数连续,光滑、严格单调,以 (0,0.5) 中心对称,是一个非常良好的阈值函数。
当输入为 0 时,sigmoid 函数的导数达到最大值 0.25; 而输入在任一方向上越远离 0 点时,导数越接近 `0`,即**当sigmoid 函数的输入很大或是很小时,它的梯度都会消失**。
目前 `sigmoid` 函数在隐藏层中已经较少使用,原因是 `sigmoid` 的软饱和性,使得深度神经网络在过去的二三十年里一直难以有效的训练,如今其被更简单、更容易训练的 `ReLU` 等激活函数所替代。
当我们想要输出二分类或多分类、多标签问题的概率时,`sigmoid` **可用作模型最后一层的激活函数**。下表总结了常见问题类型的最后一层激活和损失函数。
|问题类型|最后一层激活 |损失函数|
|-------|-----------|-------|
|二分类问题(binary)| `sigmoid` | `sigmoid + nn.BCELoss()` 模型最后一层需要经过 torch.sigmoid 函数 |
|多分类、单标签问题(Multiclass)|`softmax`|`nn.CrossEntropyLoss()`: 无需手动做 `softmax`|
|多分类、多标签问题(Multilabel)|`sigmoid`|`sigmoid + nn.BCELoss()`: 模型最后一层需要经过 `sigmoid` 函数|
> `nn.BCEWithLogitsLoss()` 函数等效于 `sigmoid + nn.BCELoss`。
### 2.2 Tanh 函数
`Tanh`(双曲正切)函数也是一种 Sigmoid 型函数,可以看作放大并平移的 `Sigmoid` 函数,公式如下所示:
$$
\text{tanh}(x) = 2\sigma(2x) - 1 = \frac{2}{1 + e^{-2x}} - 1
$$
利用基本导数公式,可得 Tanh 函数的导数公式(推导过程省略):
$$
\frac{\mathrm{d} }{\mathrm{d} x} \text{tanh}(x) = 1 - \text{tanh}^{2}(x)
$$
**Logistic 和 Tanh 两种激活函数的实现及可视化代码**(复制可直接运行)如下所示:
```python
# example plot for the sigmoid activation function
import numpy as np
from matplotlib import pyplot
import matplotlib.pyplot as plt
# sigmoid activation function
def sigmoid(x):
"""1.0 / (1.0 + exp(-x))
"""
return 1.0 / (1.0 + np.exp(-x))
def tanh(x):
"""2 * sigmoid(2*x) - 1
(e^x – e^-x) / (e^x + e^-x)
"""
# return (exp(x) - exp(-x)) / (exp(x) + exp(-x))
return 2 * sigmoid(2*x) - 1
def relu(x):
return max(0.0, x)
def gradient_relu(x):
"""1 * (x > 0)"""
if x < 0.0:
return 0
else:
return 1
def gradient_sigmoid(x):
"""sigmoid(x)(1−sigmoid(x))
"""
a = sigmoid(x)
b = 1 - a
return a*b
def gradient_tanh(x):
return 1 - tanh(x)**2
# 1, define input data
inputs = [x for x in range(-6, 7)]
# 2, calculate outputs
outputs = [sigmoid(x) for x in inputs]
outputs2 = [tanh(x) for x in inputs]
# 3, plot sigmoid and tanh function curve
plt.figure(dpi=100) # dpi 设置
plt.style.use('ggplot') # 主题设置
plt.plot(inputs, outputs, label='sigmoid')
plt.plot(inputs, outputs2, label='tanh')
plt.xlabel("x") # 设置 x 轴标签
plt.ylabel("y")
plt.title('sigmoid and tanh') # 折线图标题
plt.legend()
plt.show()
```
程序运行后得到的 Sigmoid 和 Tanh 函数曲线如下图所示:
<div align="center">
<img src="../images/activation_function/sigmoid_tanh_curve.png" width="70%" alt="Logistic函数和Tanh函数">
</div>
以上代码的基础上,改下 plt.plot 函数的输入数据,同样可得到 Tanh 函数及其导数曲线图:
<div align="center">
<img src="../images/activation_function/tanh_and_gradient_curve.png" width="70%" alt="Tanh函数及其导数">
</div>
可以看出 `Sigmoid` 和 `Tanh` 函数在输入很大或是很小的时候,**输出都几乎平滑且梯度很小趋近于 0**,不利于权重更新;不同的是 `Tanh` 函数的输出区间是在 `(-1,1)` 之间,而且整个函数是以 0 为中心的,即他本身是零均值的,也就是说,在前向传播过程中,输入数据的均值并不会发生改变,这就使他在很多应用中效果能比 `Sigmoid` 优异一些。
**`Tanh` 函数优缺点总结**:
- 具有 Sigmoid 的所有优点。
- `exp` 指数计算代价大。梯度消失问题仍然存在。
`Tanh` 函数及其导数曲线如下所示:
Tanh 和 Logistic 函数的导数很类似,都有以下特点:
- 当输入接近 0 时,导数接近最大值 1。
- 输入在任一方向上越远离0点,导数越接近0。
## 三 ReLU 函数及其变体(半线性激活函数)
### 3.1 ReLU 函数
`ReLU`(Rectified Linear Unit,修正线性单元),是目前深度神经网络中**最经常使用的激活函数**,它保留了类似 step 那样的生物学神经元机制: 输入超过阈值才会激发。公式如下所示:
$$
ReLU(x) = max(0, x) = \left \lbrace \begin{matrix}
x & x\geq 0 \\
0 & x< 0
\end{matrix}\right.
$$
以上公式通俗理解就是,`ReLU` 函数仅保留正元素并丢弃所有负元素。注意: 虽然在 `0` 点不能求导,但是并不影响其在以梯度为主的反向传播算法中发挥有效作用。
1,**优点**:
- `ReLU` 激活函数**计算简单**;
- 具有**很好的稀疏性**,大约 50% 的神经元会处于激活状态。
- 函数在 x > 0 时导数为 1 的性质(**左饱和函数**),在一定程度上缓解了神经网络的梯度消失问题,加速梯度下降的收敛速度。
> 相关生物知识: 人脑中在同一时刻大概只有 1% ∼ 4% 的神经元处于活跃状态。
2,**缺点**:
- ReLU 函数的输出是非零中心化的,给后一层的神经网络引入偏置偏移,会**影响梯度下降的效率**。
- ReLU 神经元在训练时比较容易“死亡”。如果神经元参数值在一次不恰当的更新后,其值小于 0,那么这个神经元自身参数的梯度永远都会是 0,在以后的训练过程中永远不能被激活,这种现象被称作“**死区**”。
ReLU 激活函数的代码定义如下:
```python
# pytorch 框架对应函数: nn.ReLU(inplace=True)
class ReLU(object):
def func(self, x):
return np.maximum(x, 0.0)
def derivative(self, x):
"""简单写法: return x > 0.0"""
da = np.array([1 if x > 0 else 0 for x in a])
return da
```
**ReLU 激活函数及其函数梯度图**如下所示:
<div align="center">
<img src="../images/activation_function/relu_and_gradient_curve2.png" width="70%" alt="relu_and_gradient_curve">
</div>
> `ReLU` 激活函数的更多内容,请参考原论文 [Rectified Linear Units Improve Restricted Boltzmann Machines](https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf)
### 3.2,Leaky ReLU/PReLU/ELU/Softplus 函数
1,`Leaky ReLU` **函数**: 为了缓解“**死区**”现象,研究者将 ReLU 函数中 x < 0 的部分调整为 $\gamma \cdot x$, 其中 $\gamma$ 常设置为 0.01 或 0.001 数量级的较小正数。这种新型的激活函数被称作**带泄露的 ReLU**(`Leaky ReLU`)。
$$
\text{Leaky ReLU}(x) = max(0, 𝑥) + \gamma\ min(0, x)
= \left \lbrace \begin{matrix}
x & x\geq 0 \\
\gamma \cdot x & x< 0
\end{matrix}\right.
$$
> 详情可以参考原论文:[《Rectifier Nonlinearities Improve Neural Network Acoustic Models》](https://www.semanticscholar.org/paper/Rectifier-Nonlinearities-Improve-Neural-Network-Maas/367f2c63a6f6a10b3b64b8729d601e69337ee3cc?p2df)
2,`PReLU` **函数**: 为了解决 Leaky ReLU 中**超参数 $\gamma$ 不易设定**的问题,有研究者提出了参数化 ReLU(Parametric ReLU,`PReLU`)。参数化 ReLU 直接将 $\gamma$ 也作为一个网络中可学习的变量融入模型的整体训练过程。对于第 $i$ 个神经元,`PReLU` 的 定义为:
$$
\text{Leaky ReLU}(x) = max(0, 𝑥) + \gamma_{i}\ min(0, x)
= \left\lbrace\begin{matrix}
x & x\geq 0 \\
\gamma_{i} \cdot x & x< 0
\end{matrix}\right.
$$
> 详情可以参考原论文:[《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》](https://arxiv.org/abs/1502.01852)
3,`ELU` **函数**: 2016 年,Clevert 等人提出的 `ELU` (Exponential Linear Units) 在小于零的部分采用了负指数形式。`ELU` 有很多优点,一方面作为非饱和激活函数,它在所有点上都是连续的和可微的,所以不会遇到梯度爆炸或消失的问题;另一方面,与其他线性非饱和激活函数(如 ReLU 及其变体)相比,它有着更快的训练时间和更高的准确性。
但是,与 ReLU 及其变体相比,其**指数操作也增加了计算量**,即模型推理时 `ELU` 的性能会比 `ReLU` 及其变体慢。 `ELU` 定义如下:
$$
\text{Leaky ReLU}(x) = max(0, 𝑥) + min(0, \gamma(exp(x) - 1)
= \left\lbrace\begin{matrix}
x & x\geq 0 \\
\gamma(exp(x) - 1) & x< 0
\end{matrix}\right.
$$
$\gamma ≥ 0$ 是一个超参数,决定 $x ≤ 0$ 时的饱和曲线,并调整输出均值在 `0` 附近。
> 详情可以参考原论文:[《Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)》](https://arxiv.org/abs/1511.07289)
4,`Softplus` **函数**: Softplus 函数其导数刚好是 Logistic 函数.Softplus 函数虽然也具有单侧抑制、宽 兴奋边界的特性,却没有稀疏激活性。`Softplus` 定义为:
$$
\text{Softplus}(x) = log(1 + exp(x))
$$
> 对 `Softplus` 有兴趣的可以阅读这篇论文: [《Deep Sparse Rectifier Neural Networks》](http://proceedings.mlr.press/v15/glorot11a/glorot11a.pdf)。
注意: **ReLU 函数变体有很多,但是实际模型当中使用最多的还是 `ReLU` 函数本身**。
ReLU、Leaky ReLU、ELU 以及 Softplus 函数示意图如下图所示:
<div align="center">
<img src="../images/activation_function/relu_more.png" width="70%" alt="relu_more">
</div>
## 四 Swish 函数
`Swish` 函数[Ramachandran et al., 2017] 是一种自门控(Self-Gated)激活 函数,定义为
$$
\text{swish}(x) = x\sigma(\beta x)
$$
其中 $\sigma(\cdot)$ 为 Logistic 函数,$\beta$ 为可学习的参数或一个固定超参数。$\sigma(\cdot) \in (0, 1)$ 可以看作一种软性的门控机制。当 $\sigma(\beta x)$ 接近于 `1` 时,门处于“开”状态,激活函数的输出近似于 $x$ 本身;当 $\sigma(\beta x)$ 接近于 `0` 时,门的状态为“关”,激活函数的输出近似于 `0`。
`Swish` 函数代码定义如下:
```python
# Swish https://arxiv.org/pdf/1905.02244.pdf
class Swish(nn.Module): #Swish激活函数
@staticmethod
def forward(x, beta = 1): # 此处beta默认定为1
return x * torch.sigmoid(beta*x)
```
结合前面的画曲线代码,可得 Swish 函数的示例图:
<div align="center">
<img src="../images/activation_function/swish_of_different_beta2.png" width="70%" alt="Swish 函数">
</div>
**Swish 函数可以看作线性函数和 ReLU 函数之间的非线性插值函数,其程度由参数 $\beta$ 控制**。
## 五 激活函数总结
常用的激活函数包括 `ReLU` 函数、`sigmoid` 函数和 `tanh` 函数。其标准代码总结如下(Pytorch 框架中会更复杂)
```python
from math import exp
class Sigmoid(object):
def func(self, x):
return 1.0 / (1.0 + np.exp(-x))
def derivative(self, x):
return self.func(x) * (1.0 - self.func(x))
class Tanh(object):
def func(self, x):
return np.tanh(x)
def derivative(self, x):
return 1.0 - self.func(x) ** 2
class ReLU(object):
def func(self, x):
return np.maximum(x, 0.0)
def derivative(self, x):
return x > 0.0
class LeakyReLU(object):
def __init__(self, alpha=0.2):
super().__init__()
self.alpha = alpha
def func(self, x):
return np.array([x if x > 0 else self.alpha * x for x in z])
def derivative(self, x):
dx = np.array([1 if x > 0 else self.alpha for x in a])
return dx
class Softplus(object):
def func(self, x):
return np.log(1 + np.exp(z))
def derivative(self, x):
return 1.0 / (1.0 + np.exp(-x))
```
下表汇总比较了几个激活函数的属性:
<div align="center">
<img src="../images/activation_function/activation_function_summary.png" width="100%" alt="activation_function">
</div>
**激活函数的在线可视化**移步 [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)。
## 参考资料
1. [Pytorch分类问题中的交叉熵损失函数使用](https://www.cnblogs.com/hmlovetech/p/14515622.html)
2. 《解析卷积神经网络-第8章》
3. 《神经网络与深度学习-第4章》
4. [How to Choose an Activation Function for Deep Learning](https://machinelearningmastery.com/choose-an-activation-function-for-deep-learning/)
5. [深度学习中的激活函数汇总](http://spytensor.com/index.php/archives/23/)
6. [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)
7. [AI-EDU: 挤压型激活函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/A2-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/%E7%AC%AC4%E6%AD%A5%20-%20%E9%9D%9E%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/08.1-%E6%8C%A4%E5%8E%8B%E5%9E%8B%E6%BF%80%E6%B4%BB%E5%87%BD%E6%95%B0.html)
8. https://github.com/borgwang/tinynn/blob/master/tinynn/core/layer.py
================================================
FILE: 2-deep_learning_basic/pytorch_basic/Pytorch基础-tensor数据结构.md
================================================
---
layout: post
title: Pytorch基础-tensor数据结构
date: 2021-03-07 12:00:00
summary: torch.Tensor 是一种包含单一数据类型元素的多维矩阵,类似于 numpy 的 array,本文详细介绍了 Tensor 的数据类型、属性以及如何创建。
categories: DeepLearning
---
- [Tensor 概述](#tensor-概述)
- [1.1 Tensor 数据类型](#11-tensor-数据类型)
- [1.2 Tensor 的属性](#12-tensor-的属性)
- [二 创建 Tensor](#二-创建-tensor)
- [2.1 传数据的方法创建 Tensor](#21-传数据的方法创建-tensor)
- [2.2 传 size 的方法创建 Tensor](#22-传-size-的方法创建-tensor)
- [2.3 其他创建 tensor 的方法](#23-其他创建-tensor-的方法)
- [2.4 代码示例](#24-代码示例)
- [创建张量方法总结](#创建张量方法总结)
- [参考资料](#参考资料)
## Tensor 概述
`torch.Tensor` 是一种包含**单一数据类型**元素的多维矩阵,类似于 numpy 的 `array`。

1,指定数据类型的 tensor 可以通过传递参数 `torch.dtype` 和/或者 `torch.device` 到构造函数生成:
> 注意为了改变已有的 tensor 的 torch.device 和/或者 torch.dtype, 考虑使用 `to()` 方法.
```python
>>> torch.ones([2,3], dtype=torch.float64, device="cuda:0")
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0', dtype=torch.float64)
>>> torch.ones([2,3], dtype=torch.float32)
tensor([[1., 1., 1.],
[1., 1., 1.]])
```
2,Tensor 的内容可以通过 Python 索引或者切片访问以及修改,用法和 `ndarray` 的操作一致:
```python
>>> matrix = torch.tensor([[2,3,4],[5,6,7]])
>>> print(matrix[1][2])
tensor(7)
>>> matrix[1][2] = 9
>>> print(matrix)
tensor([[2, 3, 4],
[5, 6, 9]])
```
3,使用 `torch.Tensor.item()` 或者 `int()` 方法从**只有一个值的 Tensor**中获取 Python 数值对象:
```python
>>> x = torch.tensor([[4.5]])
>>> x
tensor([[4.5000]])
>>> x.item()
4.5
>>> int(x)
4
```
4,Tensor可以通过参数 `requires_grad=True` 创建, 这样 `torch.autograd` 会记录相关的运算实现自动求导:
```python
>>> x = torch.tensor([[1., -1.], [1., 1.]], requires_grad=True)
>>> out = x.pow(2).sum()
>>> out.backward()
>>> x.grad
tensor([[ 2.0000, -2.0000],
[ 2.0000, 2.0000]])
```
5,每一个 tensor都有一个相应的 `torch.Storage` 保存其数据。tensor 类提供了一个多维的、strided 视图, 并定义了数值操作。
6,张量和 numpy 数组。可以用 `.numpy()` 方法从 Tensor 得到 numpy 数组,也可以用 `torch.from_numpy` 从 numpy 数组得到Tensor。这两种方法关联的 Tensor 和 numpy 数组是共享数据内存的。可以用张量的 `clone`方法拷贝张量,中断这种关联。
```python
arr = np.random.rand(4,5)
print(type(arr))
tensor1 = torch.from_numpy(arr)
print(type(tensor1))
arr1 = tensor1.numpy()
print(type(arr1))
"""
<class 'numpy.ndarray'>
<class 'torch.Tensor'>
<class 'numpy.ndarray'>
"""
```
7,2,`item()` 方法和 `tolist()` 方法可以将张量转换成 Python 数值和数值列表
```python
# item方法和tolist方法可以将张量转换成Python数值和数值列表
scalar = torch.tensor(5) # 标量
s = scalar.item()
print(s)
print(type(s))
tensor = torch.rand(3,2) # 矩阵
t = tensor.tolist()
print(t)
print(type(t))
"""
1.0
<class 'float'>
[[0.8211846351623535, 0.20020723342895508], [0.011571824550628662, 0.2906131148338318]]
<class 'list'>
"""
```
### 1.1 Tensor 数据类型
Torch 定义了七种 CPU Tensor 类型和八种 GPU Tensor 类型:

`torch.Tensor` 是默认的 tensor 类型(`torch.FloatTensor`)的简称,即 `32` 位浮点数数据类型。
### 1.2 Tensor 的属性
Tensor 有很多属性,包括数据类型、Tensor 的维度、Tensor 的尺寸。
+ **数据类型**:可通过改变 torch.tensor() 方法的 `dtype` 参数值,来设定不同的 `Tensor` 数据类型。
+ **维度**:不同类型的数据可以用不同维度(dimension)的张量来表示。标量为 `0` 维张量,向量为 `1` 维张量,矩阵为 `2` 维张量。彩色图像有 `rgb` 三个通道,可以表示为 `3` 维张量。视频还有时间维,可以表示为 `4` 维张量,有几个中括号 `[` 维度就是几。**可使用 `dim() 方法` 获取 `tensor` 的维度**。
+ **尺寸**:可以使用 `shape属性`或者 `size()方法`查看张量在每一维的长度,可以使用 `view()方法`或者`reshape() 方法`改变张量的尺寸。Pytorch 框架中四维张量形状的定义是 `(N, C, H, W)`。
+ **张量元素总数**:numel() 方法返回(输入)张量元素的总数。
+ **设备**:`.device` 返回张量所在的设备。
> 关于如何理解 Pytorch 的 Tensor Shape 可以参考 stackoverflow 上的这个 [回答](https://stackoverflow.com/questions/52370008/understanding-pytorch-tensor-shape)。
样例代码如下:
```python
matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],
[[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)
print(matrix) # 打印 tensor
print(matrix.dtype) # 打印 tensor 数据类型
print(matrix.dim()) # 打印 tensor 维度
print(matrix.size()) # 打印 tensor 尺寸
print(matrix.shape) # 打印 tensor 尺寸
matrix2 = matrix.view(4, 2, 2) # 改变 tensor 尺寸
print(matrix2)
print(matrix.numel())
```
```python
>>> matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],
... [[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)
>>> matrix
tensor([[[1., 2., 3., 4.],
[5., 6., 7., 8.]],
[[5., 4., 6., 7.],
[5., 6., 8., 9.]]], dtype=torch.float64)
>>> matrix.dtype # tensor 数据类型
torch.float64
>>> matrix.dim() # tensor 维度
3
>>> matrix.size() # tensor 尺寸
torch.Size([2, 2, 4])
>>> matrix.shape # tensor 形状
torch.Size([2, 2, 4])
>>> matrix.numel() # tensor 元素总数
16
>>> matrix.device
device(type='cpu')
```
## 二 创建 Tensor
创建 tensor ,可以传入数据或者维度,torch.tensor() 方法只能传入数据,torch.Tensor() 方法既可以传入数据也可以传维度,强烈建议 tensor() 传数据,Tensor() 传维度,否则易搞混。
具体来说,一般使用 torch.tensor() 方法将 python 的 `list` 或 numpy 的 `ndarray` 转换成 Tensor 数据,生成的是`dtype` 默认是 `torch.FloatTensor`,和 torch.float32 或者 torch.float 意义一样。
通过 torch.tensor() 传入数据的方法创建 tensor 时,`torch.tensor()` 总是拷贝 data 且一般不会改变原有数据的数据类型 `dtype`。如果你有一个 tensor data 并且仅仅想改变它的 `requires_grad` 属性,可用 `requires_grad_()` 或者 `detach()` 来避免拷贝。如果你有一个 `numpy` 数组并且想避免拷贝,请使用 `torch.as_tensor()`。
### 2.1 传数据的方法创建 Tensor
1,`torch.tensor()`。
将 python 的 `list` 或 numpy 的 `ndarray` 转换成 Tensor 数据。
```python
torch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False)
```
参数解释:
- `data`: 数据,可以是 list,ndarray
- `dtype`: 数据类型,默认与 data 的一致
- `device`: 所在设备,cuda/cpu
- `requires_grad`: 是否需要梯度
- `pin_memory`: 是否存于锁页内存
代码示例:
```python
>>> a = np.arange(12).reshape(3,4)
>>> b = torch.tensor(a)
>>> b # 打印张量 b 数据
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a.dtype
dtype('int64')
>>> b.dtype # 张量 b 元素的数据类型(和 numpy 数组 a 一致 )
torch.int64
>>> a.shape
(3, 4)
>>> b.shape # 张量 b 的形状
torch.Size([3, 4])
>>> b.dim() # 张量 b 的维度
2
>>> b.numel() # 张量 b 的元素个数
12
```
2,`torch.from_numpy(ndarray)`
从 numpy 创建 tensor。利用这个方法创建的 tensor 和原来的 ndarray 共享内存(不会拷贝数据,节省内存和时间),当修改其中一个数据,另外一个也会被改动。
```python
>>> arr = np.arange(1, 7).reshape(2, 3)
>>> ta = torch.from_numpy(arr)
>>> ta[1][2] = 100 # 修改第 2 行 第 3 列元素值为 100
>>> ta
tensor([[ 1, 2, 3],
[ 4, 5, 100]])
>>> arr
array([[ 1, 2, 3],
[ 4, 5, 100]])
```
3,`torch.empty_like`、`torch.zeros_like`、`torch.ones_like` 和 `torch.randint_like()`。
- 前面三个函数是根据 input(tensor 数据) 形状创建空、全 0 和全 1 的张量。
- `torch.randint_like()`:返回和输入 tensor 形状相同的张量,但是数据范围由输入参数指定,在 `[low=0, high]` 之间均匀生成的随机整数。
`torch.empty_like` 和 `torch.randint_like()` 函数声明如下所示:
```python
torch.empty_like(input, *, dtype=None,) -> Tensor
# torch.ones_like 和 torch.zeros_like 几乎一致
torch.zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor
torch.randint_like(input, low=0, high, \*, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor
```
`torch.empty_like` 和 `torch.zeros_like` 的简单函数实例如下所示:
```python
arr = np.arange(20).reshape(5, 4)
a_tensor = torch.from_numpy(arr)
b_like = torch.empty_like(a_tensor)
c_like = torch.zeros_like(a_tensor)
assert a_tensor.shape == b_like.shape == c_like.shape == torch.Size([5,4])
print(c_like.shape)
print(c_like)
"""
torch.Size([5, 4])
tensor([[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]])
"""
```
### 2.2 传 size 的方法创建 Tensor
1,`torch.empty`、`torch.zeros`、`torch.ones` 等方法。
直接传入张量形状即可创建形状为 `size` 的空、全 0 和全 1 的张量。
```python
torch.zeros(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
```
代码示例:
```python
>>> torch.zeros(4,5,6)
tensor([[[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]],
[[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]],
[[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]],
[[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]]])
```
### 2.3 其他创建 tensor 的方法
1,`torch.arange()`
功能和参数 `np.arange()` 几乎一样,默认创建区间为 `[0, end)` 公差为 $1$ 的 1维张量。
```python
torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
```
参数解释:
- start: 数列起始值
- end: 数列结束值,开区间,取不到结束值
- step: 数列公差,默认为 `1`
2,`torch.normal()`
根据给定的均值 (mean=0) 和标准差 (std=1) 从正态分布中抽取随机样本。每个生成的元素都是独立的随机变量,**遵循相同的正态分布**。
值的注意的是,当我们生成一个有限大小的样本(例如 3x3 共9个元素)时,样本的均值和标准差并不一定严格等于总体的均值和标准差。这是因为:
- 抽样误差(Sampling Error): 由于样本量有限,样本统计量(如均值、标准差)可能会偏离总体参数。这种偏差是自然的随机现象,随着样本量的增加,样本统计量会更接近总体参数。
- 有限样本量的波动: 在小样本中,随机波动对统计量的影响较大。例如,在仅有 9 个样本的情况下,个别极端值可能显著影响均值和标准差。
**参数说明**:
- `mean` (float 或 Tensor): 正态分布的均值。如果是张量,必须与 std 的形状一致。
- `std` (float 或 Tensor): 正态分布的标准差。如果是张量,必须与 mean 的形状一致。
- `size` (tuple): 输出张量的形状。
- `generator` (torch.Generator, 可选): 用于生成随机数的随机数生成器。
- `out` (Tensor, 可选): 用于存储输出结果的张量。
返回一个张量,张量中的随机数从各自的正态分布中抽取,这些正态分布的均值和标准差是给定的。
这个函数有 4 种模式,这里给出常见 `torch.normal(mean=float, std=float, size, *)` 的用法示例。
```python
torch.normal(mean, std, size, *, out=None) → Tensor
```
参数解释:
- `mean`(float):所有分布的均值
- `std`(float):所有分布的标准差
- `size`(int…):定义输出张量形状的整数序列
3,`torch.randn()` 或 `torch.randn_like()`
功能:返回一个形状为 `size` 的张量,张量中的随机数来自均值为 0、方差为 1 的正态分布(也称为标准正态分布)。
```python
torch.randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor
```
4,`torch.randint()` 和 `torch.randint_like()`
功能:**返回一个与 Tensor input 形状相同的张量,其中填充了区间 `[low, high)` 之间均匀生成的随机整数。**
```python
torch.randint(low=0, high, size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad =False)-> Tensor
```
5,`torch.tril` 和 `torch.tril` (triu 和 tril 分别是“triangle upper”和“triangle lower”的缩写。)
```python
torch.tril(input, diagonal=0, *, out=None) → Tensor
```
- `torch.tril` 函数返回输入矩阵(2D 张量)或一组矩阵的**下三角部分**,结果张量中的其余元素被设置为 0。下三角部分包含矩阵的主对角线及其以下的元素。
- `torch.triu` 函数返回一个矩阵的上三角部分(即矩阵上半部分,主对角线及其以上的元素),其余部分设为零。
参数 `diagonal` 控制要保留的对角线。如果 diagonal = 0,则保留主对角线及其以下的所有元素。正值的 diagonal 会包括主对角线上方的对角线,负值则排除主对角线下方的对角线,即表示向上或向下偏移的对角线。主对角线的索引为 ${(i, i)}$ ,其中 $i \in [0, \min \{d_1, d_2\} - 1]$, $d_1$ 和 $d_2$ 分别为矩阵的维度。
### 2.4 代码示例
```python
>>> torch.arange(12).reshape(4,3) # torch.arange() 用法
tensor([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
>>> torch.normal(mean=0, std=2, size=[1,4]) # torch.normal() 用法
tensor([[ 0.6018, -0.2399, 2.8425, 1.6153]]) # torch.randn() 用法
>>> torch.randn(4,6,2)
tensor([[[ 1.3282, -0.0920],
[ 0.4889, 0.0805],
[-0.5224, -0.5830],
[-0.7645, -0.6670],
[ 0.2376, 0.0135],
[-0.3824, 0.1190]],
[[ 1.0024, -1.6934],
[ 0.2822, -0.1121],
[ 0.1233, 0.4210],
[ 1.5558, 1.1571],
[-1.9819, -1.0007],
[ 1.7181, 0.5641]],
[[ 0.2924, 0.7369],
[ 0.4954, -2.3034],
[-1.1726, 0.7474],
[-0.1254, -0.2139],
[-0.3428, 1.2906],
[ 1.2389, -0.5154]],
[[ 0.8589, 2.7191],
[-0.0905, 0.3279],
[ 1.8878, 0.6622],
[-0.1519, 0.4263],
[-0.9688, 1.2181],
[-2.0909, -0.3234]]])
>>> torch.randint(10, (2,5)) # torch.randint() 用法
tensor([[3, 8, 7, 3, 5],
[9, 2, 2, 9, 6]])
>>> torch.randn(4,5).tril() # 创建下三角矩阵
tensor([[-0.8062, 0.0000, 0.0000, 0.0000, 0.0000],
[ 1.4425, 0.8054, 0.0000, 0.0000, 0.0000],
[ 0.9237, 0.8548, 0.2412, 0.0000, 0.0000],
[-0.3921, -0.4880, -0.8847, -0.2170, 0.0000]])
>>> torch.randn(4,5).triu() # 创建上三角矩阵
tensor([[-0.7354, -0.9383, -0.0798, -0.6155, 0.3702],
[ 0.0000, 0.5686, 0.2166, -0.5724, -0.0596],
[ 0.0000, 0.0000, 0.0751, -0.9832, 1.5582],
[ 0.0000, 0.0000, 0.0000, -0.0491, 1.3136]])
```
检验 torch.normal 和 torch.randn 的均值情况:
```python
import torch
def generate_tensor_and_print_stats(size, func="normal", mean=0.0, std=1.0):
"""
生成一个张量并打印其均值和标准差。
参数:
- size (tuple): 生成张量的形状。
- func (str): 使用的生成函数。选项包括 "normal" 和 "randn"。
- mean (float, 可选): 正态分布的均值,仅在 func="normal" 时使用。默认值为0.0。
- std (float, 可选): 正态分布的标准差,仅在 func="normal" 时使用。默认值为1.0。
返回:
- tensor (Tensor): 生成的张量。
"""
if func == "normal":
tensor = torch.normal(mean=mean, std=std, size=size)
elif func == "randn":
tensor = torch.randn(size=size)
else:
raise ValueError(f"无效的函数类型: {func}. 请使用 'normal' 或 'randn'。")
tensor_mean = tensor.mean().item()
tensor_std = tensor.std().item()
print(f"Tensor Size: {size}, by torch.{func}, Mean: {tensor_mean:.4f}, Std Dev: {tensor_std:.4f}")
return tensor
def main():
# 设置随机种子以确保结果可重复(可选)
torch.manual_seed(40)
# 定义不同的张量大小
sizes = [(3, 3), (100, 100), (1000, 1000)]
functions = ["normal", "randn"]
# 生成并打印统计信息
for func in functions:
for size in sizes:
if func == "normal":
generate_tensor_and_print_stats(size=size, func=func, mean=0.0, std=1.0)
elif func == "randn":
generate_tensor_and_print_stats(size=size, func=func)
if __name__ == "__main__":
main()
```
程序运行后输出结果如下所示:
```bash
Tensor Size: (3, 3), by torch.normal, Mean: 0.4151, Std Dev: 0.6608
Tensor Size: (100, 100), by torch.normal, Mean: -0.0135, Std Dev: 0.9947
Tensor Size: (1000, 1000), by torch.normal, Mean: 0.0002, Std Dev: 1.0006
Tensor Size: (3, 3), by torch.randn, Mean: -0.7619, Std Dev: 1.1099
Tensor Size: (100, 100), by torch.randn, Mean: -0.0133, Std Dev: 1.0029
Tensor Size: (1000, 1000), by torch.randn, Mean: -0.0001, Std Dev: 1.0001
```
解释:
- $3\times 3$ 张量: 样本均值和 0、标准差和 1 比都有较大的偏差。
- $100\times 100$ 张量: 偏差减小,均值更接近0,标准差更接近1。
- $1000\times 1000$ 张量: 偏差进一步减小,**均值非常接近0,标准差非常接近1**。
### 创建张量方法总结
|方法名|方法功能|备注|
|-----|-------|---|
|`torch.rand(*sizes, out=None) → Tensor`|返回一个张量,包含了从区间 `[0, 1)` 的**均匀分布**中抽取的一组随机数。张量的形状由参数sizes定义。|推荐|
|`torch.randn(*sizes, out=None) → Tensor`|返回一个张量,包含了从**标准正态分布**(均值为0,方差为1,即高斯白噪声)中抽取的一组随机数。张量的形状由参数sizes定义。|不推荐|
|`torch.normal(means, std, out=None) → Tensor`|返回一个张量,包含了从指定均值 `means` 和标准差 `std` 的离散正态分布中抽取的一组随机数。标准差 `std` 是一个张量,包含每个输出元素相关的正态分布标准差。|多种形式,建议看源码|
|`torch.rand_like(a)`|根据数据 `a` 的 shape 来生成随机数据|不常用|
|`torch.randint(low=0, high, size)`|生成指定范围(`low, hight`)和 `size` 的随机整数数据|常用|
|`torch.full([2, 2], 4)`|生成给定维度,全部数据相等的数据|不常用|
|`torch.arange(start=0, end, step=1, *, out=None)`|生成指定间隔的数据|易用常用|
|`torch.ones(*size, *, out=None)`|生成给定 size 且值全为1 的矩阵数据|简单|
|`zeros()/zeros_like()/eye()`|全 `0` 的 tensor 和 对角矩阵|简单|
样例代码:
```python
>>> torch.rand([1,1,3,3])
tensor([[[[0.3005, 0.6891, 0.4628],
[0.4808, 0.8968, 0.5237],
[0.4417, 0.2479, 0.0175]]]])
>>> torch.normal(2, 3, size=(1, 4))
tensor([[3.6851, 3.2853, 1.8538, 3.5181]])
>>> torch.full([2, 2], 4)
tensor([[4, 4],
[4, 4]])
>>> torch.arange(0,10,2)
tensor([0, 2, 4, 6, 8])
>>> torch.eye(3,3)
tensor([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
```
## 参考资料
+ [PyTorch:view() 与 reshape() 区别详解](https://blog.csdn.net/Flag_ing/article/details/109129752)
+ [torch.rand和torch.randn和torch.normal和linespace()](https://zhuanlan.zhihu.com/p/115997577)
================================================
FILE: 2-deep_learning_basic/pytorch_basic/Pytorch基础-张量数学运算.md
================================================
---
layout: post
title: Pytorch基础-张量数学运算
date: 2021-03-03 12:00:00
summary: PyTorch 张量数学运算就是对张量的元素值完成数学运算,常用的张量数学运算包括:标量运算、向量运算、矩阵运算。
categories: DeepLearning
---
- [一 理解张量维度](#一-理解张量维度)
- [二 理解 dim 参数](#二-理解-dim-参数)
- [三 规约计算](#三-规约计算)
- [3.1 torch.mean](#31-torchmean)
- [3.2 torch.sum](#32-torchsum)
- [3.3 torch.max](#33-torchmax)
- [3.4 torch.cumprod](#34-torchcumprod)
PyTorch 张量数学运算就是对张量的元素值完成数学运算,常用的张量数学运算包括:标量运算、向量运算、矩阵运算。
## 一 理解张量维度
在 PyTorch 中,张量的维度(或称为“秩”)决定了数据的结构和形状:
- 1D 张量:向量。例如,长度为 5 的向量 [1, 2, 3, 4, 5]。
- 2D 张量:矩阵。例如,形状为 (3, 4) 的矩阵。
- 3D 张量:通常用于 NLP,形状为 (batch_size, sequence_length, hidden_size)。
- 4D 张量:通常用于 CV,形状为 (batch_size, channels, height, width)。
在一个 $M$ 行 $N$ 列的二维数组中,$M$ 是第 0 维,即行数;$N$ 是第 1 维,即列数。那么怎么肉眼判断更复杂的张量数据维度呢,举例:
```python
import torch
# 示例张量
tensor = torch.tensor([[[0.6238, -0.9315, 0.2173, 0.1954, -1.1565],
[0.4559, 0.1531, 0.4178, 1.0225, 0.5923],
[0.0499, 0.4024, -1.2547, -0.5042, -0.0231],
[-1.1253, 0.3145, 0.8796, 0.4516, -0.0915]],
[[1.5794, -0.6367, -0.2559, 0.1237, -0.1951],
[0.1012, 0.0357, -0.5699, 1.0983, -0.2084],
[-0.7019, 0.5872, 0.7736, 0.7423, -0.7894],
[-0.3248, -0.5316, 1.2029, 0.2852, -0.4565]],
[[-0.0073, 1.4143, -0.1859, -0.7211, -0.8652],
[-0.3173, -0.4816, 0.1174, -0.1554, 0.9385],
[0.1283, -0.6547, 0.3687, -0.1948, 0.7754],
[-0.2185, -1.0437, 1.5963, -0.3284, -0.3654]]])
```
**判断规则**:方括号 `[` 的嵌套层数代表张量的维度。最外层括号的元素数量是第 0 维的大小,往内推。
以上述张量为例分析:
```python
tensor([[[ 0.6238, -0.9315, 0.2173, 0.1954, -1.1565], ... ]])
```
- 最外层 [ 里有 3 个子列表 -> 第 0 维大小为 3。
- 第二层 [ 里有 4 个子列表 -> 第 1 维大小为 4。
- 第三层 [ 里有 5 个元素 -> 第 2 维大小为 5。
因此,这个张量是 3 维张量,形状为 `[3, 4, 5]`。
## 二 理解 dim 参数
**`dim` 参数在 pytorch 数学函数中的定义一般指沿着 dim 这个维度进行操作**:求和/求平均/求累加,以及删除、增加指定 dim。如 x 的 shape 为 [2, 5, 3],则:
- `dim = 0`,即沿着具有 2 个元素的那个维度/轴进行操作
- `dim = 1`,即沿着具有 5 个元素的那个维度/轴进行操作
- `dim = 2`,即沿着具有 3 个元素的那个维度/轴进行操作
再看具体示例:
```bash
>>> x = torch.randint(1, 10, [2,5,3], dtype=torch.float32)
>>> x
tensor([[[8., 1., 6.],
[1., 5., 9.],
[5., 7., 1.],
[5., 1., 2.],
[8., 4., 4.]],
[[3., 3., 7.],
[7., 4., 3.],
[7., 7., 5.],
[8., 3., 9.],
[1., 1., 8.]]])
>>> x.shape
torch.Size([2, 5, 3])
# 如执行 y = torch.mean(x, dim = 0),则 y[0,0] = (x[0, 0, 0] + x[1, 0, 0]) / 2 = (8. + 3.) / 2 = 5.5; y[2, 2] = (x[0, 2, 2] + x[1, 2, 2]) = (1. + 5.) / 2 = 3.0
>>> y = torch.mean(x, dim = 0)
>>> y
tensor([[5.5000, 2.0000, 6.5000],
[4.0000, 4.5000, 6.0000],
[6.0000, 7.0000, 3.0000],
[6.5000, 2.0000, 5.5000],
[4.5000, 2.5000, 6.0000]])
# 如执行 y = torch.mean(x, dim = 2), 则 y[1, 4] = (x[1, 4, 0] + x[1, 4, 1] + x[1, 4, 2]) / 3
>>> y = torch.mean(x, dim = 2)
>>> y
tensor([[5.0000, 5.0000, 4.3333, 2.6667, 5.3333],
[4.3333, 4.6667, 6.3333, 6.6667, 3.3333]])
```
## 三 规约计算
规约计算一般是指分组聚合计算,表现结果就是会进行维度压缩。
### 3.1 torch.mean
torch.mean 函数用于计算张量沿指定维度的平均值。其基本语法和参数解释如下:
```python
torch.mean(input, dim, keepdim=False, *, dtype=None) -> Tensor
```
- `input`:输入张量。
- `dim`:沿哪个维度计算平均值。可以是单个整数或整数元组。
- `keepdim`:是否保留被缩减的维度。默认为 False。
- `dtype`:输出张量的数据类型。
1. 从计算过程理解:**沿着(跨) dim 进行操作(算均值)**。
- 常规矩阵操作的 2D 张量,dim = 0 表示跨行操作,即对每一列中的所有元素进行均值计算。
- NLP 领域的 3D 张量 `(batch_size, sequence_length, embedding_size)`,`dim = 2` 表示跨嵌入层维度算均值,对于每个 (batch, sequence) 位置,计算嵌入维度上的均值,如创建一个形状为 (4, 16, 4) 的 3D 张量计算位置 (0, 0, \:) 的均值 $\text{mean}(x[0, 0:]) = \frac{x[0,0,0] + x[0,0,1] + x[0,0,2] + x[0,0,3]}{4}$。
- CV 领域的 4D 张量 `(batch_size, channels, height, width)`,`dim = 0` 表示跨 batch_size 维度上计算均值,对一个批次中的所有样本进行平均。如创建一个形状为 (2, 3, 3, 3) 的 4D 张量,计算位置 (0, 0, 0) 的均值 = $\text{mean}(x[:, 0, 0, 0]) = \frac{x[0, 0, 0, 0] + x[1, 0, 0, 0]}{2}$。
2. 从输出张量的形状理解:
- NLP 领域的 3D 张量,dim = 2,输出张量的形状去掉这个 dim 维度,得到输出张量形状为 `(batch_size, sequence_length)`。
- CV 领域的 4D 张量,dim = 0,输出张量形状为 `(channels, height, width)`。
3. 是否保留维度参数的理解:
- keepdim=False(默认):求和后,指定的维度会被压缩,即结果张量的维度将减少。
- keepdim=True:求和后,指定的维度将保留,且其大小为 1,这对于后续需要特定维度的操作(如广播机制)非常有用。
### 3.2 torch.sum
`torch.sum` 沿着指定维度求和。
```bash
>>> x = torch.randn([4,5])
>>> x
tensor([[ 1.1141, 1.7091, -0.5543, 0.3417, -0.0838],
[-0.6697, -0.3165, 0.3772, -0.4377, -0.9850],
[-1.3976, 1.3172, -0.6791, 0.1030, -0.5817],
[ 0.3079, -0.5911, 1.2357, -1.0891, 0.8422]])
>>> x.sum(dim=0)
tensor([-0.6453, 2.1187, 0.3796, -1.0821, -0.8082])
>>> x.sum(dim=1)
tensor([ 2.5269, -2.0317, -1.2381, 0.7056])
```
当 dim = 0 时,就是沿着 `dim = 0`即 `x` 轴进行累加,sum 函数为规约函数会压缩维度,所以x.sum(dim=0) 结果为 tensor([-0.6453, 2.1187, 0.3796, -1.0821, -0.8082]),形状为 `[5]`。
### 3.3 torch.max
`torch.max()` 用于获取张量的最大值,其有三种用法:
- 获取张量中的最大值。
- 沿指定维度获取最大值及其索引。
- 逐元素比较两个张量,返回最大值,用法等同 `torch.minimum()` 函数
这三种用法的各自语法如下:
```python
torch.max(input) -> Tensor
torch.max(input, dim, keepdim=False, *, out=None)
torch.min(input, other, *, out=None) -> Tensor # other 也是张量
```
- 第一种用法好理解,输入参数是一个多维张量,返回的结果是这个张量的**全局最大值**!
- 第二种情况是适用于我们需要特定维度的最大值,且返回结果包含两个数据:一个是最大值对应的位置索引,一个是最大值本身,这个函数是量化算法的核心操作之一!
- 第三种情况,用于对输入的两个张量 `input` 和 `other`中的每个元素进行比较,返回对应位置的最小值。
```python
>>> x = torch.randint(10, [2,5])
>>> x
tensor([[1, 6, 5, 7, 7],
[5, 3, 2, 2, 6]])
>>> y = torch.randint(2, 12, [2,5])
>>> y
tensor([[ 8, 3, 8, 7, 4],
[11, 6, 2, 5, 3]])
>>> torch.max(x, y)
tensor([[ 8, 6, 8, 7, 7],
[11, 6, 2, 5, 6]])
>>> torch.max(x)
tensor(7)
>>> max_x = torch.max(x, dim=0) # 返回张量 x 在 dim=0 维度的最大值及对应索引
>>> max_x
torch.return_types.max(
values=tensor([5, 6, 5, 7, 7]),
indices=tensor([1, 0, 0, 0, 0]))
>>> max_x[0]
tensor([5, 6, 5, 7, 7])
```
`torch.min` 函数和 `torch.max()` 意义相同,只不过返回的是最小值。
### 3.4 torch.cumprod
`torch.cumprod` 张量沿着指定 dim 维度计算累积乘积,其返回一个与 `input` 形状相同的张量,返回张量的每个元素是指定维度上该元素及其之前所有元素的乘积。函数定义(语法)如下:
```python
torch.cumprod(input, dim, *, dtype=None, out=None) -> Tensor
```
这个沿着 dim 方向计算累积的计算过程直观上不好理解,可以参考下述计算过程的可视化分解图来理解。
<div align="center">
<img src="../images/pytorch_tensor_math/dim_compute_visual.png" width="80%" alt="dim_compute_visual">
</div>
多维张量的示例代码如下所示:
```python
import torch
# 创建一个 2D 张量
b = torch.tensor([[1, 2, 3], [4, 5, 6]])
print("原始张量:\n", b)
# 沿第 0 维计算累积乘积
cumprod_b_dim0 = torch.cumprod(b, dim=0)
print("沿第 0 维的累积乘积结果:\n", cumprod_b_dim0)
# 沿第 1 维计算累积乘积
cumprod_b_dim1 = torch.cumprod(b, dim=1)
print("沿第 1 维的累积乘积结果:\n", cumprod_b_dim1)
```
程序运行后输出结果如下:
```bash
原始张量:
tensor([[1, 2, 3],
[4, 5, 6]])
沿第 0 维的累积乘积结果:
tensor([[ 1, 2, 3],
[ 4, 10, 18]])
沿第 1 维的累积乘积结果:
tensor([[ 1, 2, 6],
[ 4, 20, 120]])
```
总结:上述 Tensor 数学计算函数的用法在返回张量形状变化的示例对比。
```bash
>>> x = torch.Tensor([ # shape is [2, 5]
... [2,3,4,5,6],
... [9,8,7,6,5,]
... ])
>>> print(torch.cumprod(x, dim = 0))
tensor([[ 2., 3., 4., 5., 6.],
[18., 24., 28., 30., 30.]])
>>> print(torch.cumprod(x, dim = 1)) # output shape is [2, 5]
tensor([[2.0000e+00, 6.0000e+00, 2.4000e+01, 1.2000e+02, 7.2000e+02],
[9.0000e+00, 7.2000e+01, 5.0400e+02, 3.0240e+03, 1.5120e+04]])
>>> torch.min(x, dim = 0) # output shape is [5]
torch.return_types.min(
values=tensor([2., 3., 4., 5., 5.]),
indices=tensor([0, 0, 0, 0, 1]))
>>> print(torch.max(x, dim = 1)) # output shape is [2]
torch.return_types.max(
values=tensor([6., 9.]),
indices=tensor([4, 0]))
>>> torch.mean(x, dim = 0, keepdim = True) # output shape is [1, 5]
tensor([[5.5000, 5.5000, 5.5000, 5.5000, 5.5000]])
```
================================================
FILE: 2-deep_learning_basic/pytorch_basic/Pytorch基础-张量结构操作.md
================================================
- [一,张量的基本操作](#一张量的基本操作)
- [1.1 改变形状: view 和 reshape](#11-改变形状-view-和-reshape)
- [1.2 张量拼接](#12-张量拼接)
- [二,维度变换](#二维度变换)
- [2.1 unsqueeze vs squeeze 维度增减](#21-unsqueeze-vs-squeeze-维度增减)
- [2.2,transpose vs permute 维度交换](#22transpose-vs-permute-维度交换)
- [三 索引切片](#三-索引切片)
- [3.1 规则索引切片方式](#31-规则索引切片方式)
- [3.2 gather 和 torch.index\_select 算子](#32-gather-和-torchindex_select-算子)
- [四,合并分割](#四合并分割)
- [4.1,torch.cat 和 torch.stack](#41torchcat-和-torchstack)
- [4.2 torch.split 和 torch.chunk](#42-torchsplit-和--torchchunk)
- [五 卷积相关算子](#五-卷积相关算子)
- [5.1 上采样方法总结](#51-上采样方法总结)
- [5.2,F.interpolate 采样函数](#52finterpolate-采样函数)
- [5.3 nn.ConvTranspose2d 反卷积](#53-nnconvtranspose2d-反卷积)
- [参考资料](#参考资料)
> 授人以鱼不如授人以渔,原汁原味的知识才更富有精华,本文只是对张量基本操作知识的理解和学习笔记,看完之后,想要更深入理解,建议去 pytorch 官方网站,查阅相关函数和操作,英文版在[这里](https://pytorch.org/docs/1.7.0/torch.html),中文版在[这里](https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch/#tensors)。本文的代码是在 `pytorch1.7` 版本上测试的,其他版本一般也没问题。
## 一,张量的基本操作
`Pytorch` 中,张量的操作分为**结构操作和数学运算**,其理解就如字面意思。结构操作就是改变张量本身的结构,数学运算就是对张量的元素值完成数学运算。
+ 常使用的张量结构操作:维度变换(`tranpose`、`view` 等)、合并分割(`split`、`chunk`等)、索引切片(`index_select`、`gather` 等)。
+ 常用的张量数学运算:标量运算、向量运算、矩阵运算。
### 1.1 改变形状: view 和 reshape
两个方法都是用来改变 tensor 的 shape,**view() 只适合对满足连续性条件(`contiguous`)的 tensor 进行操作**,而 reshape() 同时还可以对不满足连续性条件的 tensor 进行操作。在满足 tensor 连续性条件(`contiguous`)时,a.reshape() 返回的结果与 a.view() 相同,都不会开辟新内存空间;不满足 `contiguous` 时, 直接使用 view() 方法会失败,`reshape()` 依然有用,但是会重新开辟内存空间,不与之前的 tensor 共享内存,即返回的是 **”副本“**(等价于先调用 `contiguous()` 方法再使用 `view()` 方法)。
`view` 和 `reshape` 的区别总结如下表所示:
| 特性 | view | reshape |
|--------|-----------------------------------------------|----------------------------------------------|
| **依赖存储布局** | 需要张量的内存是连续的。如果张量内存不连续,会报错。 | 不依赖内存是否连续,会创建新的内存副本(如果必要)。 |
| **性能** | 更快,因为它只是在内存上重新定义视图,不移动数据。 | 更灵活,但可能会牺牲一些性能。 |
| **语义** | 是对原张量的视图,修改新张量会影响原张量的数据。 | 可能创建新张量,原张量不会受到影响。 |
> 更多理解可以参考这篇[文章](https://blog.csdn.net/Flag_ing/article/details/109129752)。
### 1.2 张量拼接
`torch.concat`(别名 torch.cat)用于沿着指定维度拼接张量的操作。语法:`torch.cat(tensors, dim=0)`。
- `tensors`:一个包含待拼接张量的序列(如列表或元组)。
- `dim`:指定沿哪一个维度进行拼接。
示例代码
```bash
>>> import torch
>>> A = torch.tensor([[1, 2], [3, 4]]) # 形状 [2, 2]
>>> B = torch.tensor([[5, 6], [7, 8]]) # 形状 [2, 2]
>>> C = torch.concat([A, B], dim = 0)
>>> C
tensor([[1, 2],
[3, 4],
[5, 6],
[7, 8]])
>>> C2 = torch.concat([A, B], dim = 1)
>>> C2
tensor([[1, 2, 5, 6],
[3, 4, 7, 8]])
```
## 二,维度变换
### 2.1 unsqueeze vs squeeze 维度增减
- `torch.squeeze`: 默认用于移除张量中所有大小为 1 的维度。
- `torch.unsqueeze`: 用于在指定位置插入一个大小为 1 的维度,从而增加张量的维度。
```python
# 参数, input:输入张量。dim(可选):指定要移除的维度。如果未指定,则移除所有大小为 1 的维度。
torch.squeeze(input, dim=None)
# 参数, input:输入张量。dim:指定插入维度的位置。
torch.unsqueeze(input, dim)
```
> “Squeeze” 单词作动词时,主要意思是“挤压、捏、榨取”
`squeeze` 用例程序如下:
```python
a = torch.rand(1,1,3,3)
b = torch.squeeze(a)
c = a.squeeze(1)
print(b.shape)
print(c.shape)
```
程序输出结果如下:
> torch.Size([3, 3])
torch.Size([1, 3, 3])
`unsqueeze` 用例程序如下:
```python
x = torch.rand(3,3)
y1 = torch.unsqueeze(x, 0)
y2 = x.unsqueeze(0)
print(y1.shape)
print(y2.shape)
```
程序输出结果如下:
> torch.Size([1, 3, 3])
torch.Size([1, 3, 3])
### 2.2,transpose vs permute 维度交换
`torch.transpose()` 只能交换两个维度,而 `.permute()` 可以自由交换任意位置。函数定义如下:
```python
transpose(dim0, dim1) → Tensor # See torch.transpose()
permute(*dims) → Tensor # dim(int). Returns a view of the original tensor with its dimensions permuted.
```
在 `CNN` 模型中,我们经常遇到交换维度的问题,举例:四个维度表示的 tensor:`[batch, channel, h, w]`(`nchw`),如果想把 `channel` 放到最后去,形成`[batch, h, w, channel]`(`nhwc`),如果使用 `torch.transpose()` 方法,至少要交换两次(先 `1 3` 交换再 `1 2` 交换),而使用 `.permute()` 方法只需一次操作,更加方便。例子程序如下:
```python
import torch
input = torch.rand(1,3,28,32) # torch.Size([1, 3, 28, 32]
print(b.transpose(1, 3).shape) # torch.Size([1, 32, 28, 3])
print(b.transpose(1, 3).transpose(1, 2).shape) # torch.Size([1, 28, 32, 3])
print(b.permute(0,2,3,1).shape) # torch.Size([1, 28, 28, 3]
```
## 三 索引切片
### 3.1 规则索引切片方式
张量的索引切片方式和 `numpy`、python 多维列表几乎一致,都可以通过索引和切片对部分元素进行修改。切片时支持缺省参数和省略号。实例代码如下:
```python
>>> t = torch.randint(1,10,[3,3])
>>> t
tensor([[8, 2, 9],
[2, 5, 9],
[3, 9, 9]])
>>> t[0] # 第 1 行数据
tensor([8, 2, 9])
>>> t[2][2]
tensor(9)
>>> t[0:3,:] # 第1至第3行,全部列
tensor([[8, 2, 9],
[2, 5, 9],
[3, 9, 9]])
>>> t[0:2,:] # 第1行至第2行
tensor([[8, 2, 9],
[2, 5, 9]])
>>> t[1:,-1] # 第2行至最后行,最后一列
tensor([9, 9])
>>> t[1:,::2] # 第1行至最后行,第0列到最后一列每隔两列取一列
tensor([[2, 9],
[3, 9]])
```
以上切片方式相对规则,对于不规则的切片提取,可以使用 `torch.index_select`, `torch.take`, `torch.gather`, `torch.masked_select`。
### 3.2 gather 和 torch.index_select 算子
> `gather` 算子的用法比较难以理解,在翻阅了官方文档和网上资料后,我有了一些自己的理解。
1,`gather` 是不规则的切片提取算子(Gathers values along an axis specified by dim. 在指定维度上根据索引 index 来选取数据)。函数定义如下:
```python
torch.gather(input, dim, index, *, sparse_grad=False, out=None) → Tensor
```
**参数解释**:
+ `input` (Tensor) – the source tensor.
+ `dim` (int) – the axis along which to index.
+ `index` (LongTensor) – the indices of elements to gather.
`gather` 算子的注意事项:
+ 输入 `input` 和索引 `index` 具有相同数量的维度,即 `input.shape = index.shape`
+ 对于任意维数,只要 `d != dim`,index.size(d) <= input.size(d),即对于可以不用索引维数 `d` 上的全部数据。
+ 输出 `out` 和 索引 `index` 具有相同的形状。输入和索引不会相互广播。
对于 3D tensor,`output` 值的定义如下:
`gather` 的官方定义如下:
```python
out[i][j][k] = input[index[i][j][k]][j][k] # if dim == 0
out[i][j][k] = input[i][index[i][j][k]][k] # if dim == 1
out[i][j][k] = input[i][j][index[i][j][k]] # if dim == 2
```
通过理解前面的一些定义,相信读者对 `gather` 算子的用法有了一个基本了解,下面再结合 2D 和 3D tensor 的用例来直观理解算子用法。
(1),对于 2D tensor 的例子:
```python
>>> import torch
>>> a = torch.arange(0, 16).view(4,4)
>>> a
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
>>> index = torch.tensor([[0, 1, 2, 3]]) # 选取对角线元素
>>> torch.gather(a, 0, index)
tensor([[ 0, 5, 10, 15]])
```
`output` 值定义如下:
```shell
# 按照 index = tensor([[0, 1, 2, 3]])顺序作用在行上索引依次为0,1,2,3
a[0][0] = 0
a[1][1] = 5
a[2][2] = 10
a[3][3] = 15
```
(2),索引更复杂的 2D tensor 例子:
```python
>>> t = torch.tensor([[1, 2], [3, 4]])
>>> t
tensor([[1, 2],
[3, 4]])
>>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]]))
tensor([[ 1, 1],
[ 4, 3]])
```
`output` 值的计算如下:
```shell
output[i][j] = input[i][index[i][j]] # if dim = 1
output[0][0] = input[0][index[0][0]] = input[0][0] = 1
output[0][1] = input[0][index[0][1]] = input[0][0] = 1
output[1][0] = input[1][index[1][0]] = input[1][1] = 4
output[1][1] = input[1][index[1][1]] = input[1][0] = 3
```
总结:**可以看到 `gather` 是通过将索引在指定维度 `dim` 上的值替换为 `index` 的值,但是其他维度索引不变的情况下获取 `tensor` 数据**。直观上可以理解为对矩阵进行重排,比如对每一行(dim=1)的元素进行变换,比如 `torch.gather(a, 1, torch.tensor([[1,2,0], [1,2,0]]))` 的作用就是对 矩阵 `a` 每一行的元素,进行 `permtute(1,2,0)` 操作。
2,理解了 `gather` 再看 `index_select` 就很简单,函数作用是返回沿着输入张量的指定维度的指定索引号进行索引的张量子集。函数定义如下:
```python
torch.index_select(input, dim, index, *, out=None) → Tensor
```
函数返回一个新的张量,它使用数据类型为 `LongTensor` 的 `index` 中的条目沿维度 `dim` 索引输入张量。返回的张量具有与原始张量(输入)相同的维数。 维度尺寸与索引长度相同; 其他尺寸与原始张量中的尺寸相同。实例代码如下:
```python
>>> x = torch.randn(3, 4)
>>> x
tensor([[ 0.1427, 0.0231, -0.5414, -1.0009],
[-0.4664, 0.2647, -0.1228, -1.1068],
[-1.1734, -0.6571, 0.7230, -0.6004]])
>>> indices = torch.tensor([0, 2])
>>> torch.index_select(x, 0, indices)
tensor([[ 0.1427, 0.0231, -0.5414, -1.0009],
[-1.1734, -0.6571, 0.7230, -0.6004]])
>>> torch.index_select(x, 1, indices)
tensor([[ 0.1427, -0.5414],
[-0.4664, -0.1228],
[-1.1734, 0.7230]])
```
## 四,合并分割
### 4.1,torch.cat 和 torch.stack
可以用 `torch.cat` 方法和 `torch.stack` 方法将多个张量合并,也可以用 `torch.split`方法把一个张量分割成多个张量。`torch.cat` 和 `torch.stack` 有略微的区别,`torch.cat` 是连接,不会增加维度,而 `torch.stack` 是堆叠,会增加一个维度。两者函数定义如下:
```python
# Concatenates the given sequence of seq tensors in the given dimension. All tensors must either have the same shape (except in the concatenating dimension) or be empty.
torch.cat(tensors, dim=0, *, out=None) → Tensor
# Concatenates a sequence of tensors along **a new** dimension. All tensors need to be of the same size.
torch.stack(tensors, dim=0, *, out=None) → Tensor
```
`torch.cat` 和 `torch.stack` 用法实例代码如下:
```python
>>> a = torch.arange(0,9).view(3,3)
>>> b = torch.arange(10,19).view(3,3)
>>> c = torch.arange(20,29).view(3,3)
>>> cat_abc = torch.cat([a,b,c], dim=0)
>>> print(cat_abc.shape)
torch.Size([9, 3])
>>> print(cat_abc)
tensor([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[10, 11, 12],
[13, 14, 15],
[16, 17, 18],
[20, 21, 22],
[23, 24, 25],
[26, 27, 28]])
>>> stack_abc = torch.stack([a,b,c], axis=0) # torch中dim和axis参数名可以混用
>>> print(stack_abc.shape)
torch.Size([3, 3, 3])
>>> print(stack_abc)
tensor([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8]],
[[10, 11, 12],
[13, 14, 15],
[16, 17, 18]],
[[20, 21, 22],
[23, 24, 25],
[26, 27, 28]]])
>>> chunk_abc = torch.chunk(cat_abc, 3, dim=0)
>>> chunk_abc
(tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]),
tensor([[10, 11, 12],
[13, 14, 15],
[16, 17, 18]]),
tensor([[20, 21, 22],
[23, 24, 25],
[26, 27, 28]]))
```
另外,`torch.hstack` 函数用于将一系列张量沿水平方向(列方向)堆叠。对于一维张量,相当于直接按元素连接;对于二维及更高维张量,相当于沿第二个维度(列维度)堆叠。它是 torch.cat(tensors, dim=1) 的简便形式。
两个二维张量的 `hstack` 操作过程可视化如下图所示:

### 4.2 torch.split 和 torch.chunk
`torch.split()` 和 `torch.chunk()` 可以看作是 `torch.cat()` 的逆运算。`split()` 作用是将张量拆分为多个块,每个块都是原始张量的视图。`split()` [函数定义](https://pytorch.org/docs/stable/generated/torch.split.html#torch.split)如下:
```python
"""
Splits the tensor into chunks. Each chunk is a view of the original tensor.
If split_size_or_sections is an integer type, then tensor will be split into equally sized chunks (if possible). Last chunk will be smaller if the tensor size along the given dimension dim is not divisible by split_size.
If split_size_or_sections is a list, then tensor will be split into len(split_size_or_sections) chunks with sizes in dim according to split_size_or_sections.
"""
torch.split(tensor, split_size_or_sections, dim=0)
```
`chunk()` 作用是将 `tensor` 按 `dim`(行或列)分割成 `chunks` 个 `tensor` 块,返回的是一个元组。`chunk()` 函数定义如下:
```python
torch.chunk(input, chunks, dim=0) → List of Tensors
"""
Splits a tensor into a specific number of chunks. Each chunk is a view of the input tensor.
Last chunk will be smaller if the tensor size along the given dimension dim is not divisible by chunks.
Parameters:
input (Tensor) – the tensor to split
chunks (int) – number of chunks to return
dim (int) – dimension along which to split the tensor
"""
```
实例代码如下:
```python
>>> a = torch.arange(10).reshape(5,2)
>>> a
tensor([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
>>> torch.split(a, 2)
(tensor([[0, 1],
[2, 3]]),
tensor([[4, 5],
[6, 7]]),
tensor([[8, 9]]))
>>> torch.split(a, [1,4])
(tensor([[0, 1]]),
tensor([[2, 3],
[4, 5],
[6, 7],
[8, 9]]))
>>> torch.chunk(a, 2, dim=1)
(tensor([[0],
[2],
[4],
[6],
[8]]),
tensor([[1],
[3],
[5],
[7],
[9]]))
```
## 五 卷积相关算子
### 5.1 上采样方法总结
上采样大致被总结成了三个类别:
1. 基于线性插值的上采样:最近邻算法(`nearest`)、双线性插值算法(`bilinear`)、双三次插值算法(`bicubic`)等,这是传统图像处理方法。
2. 基于深度学习的上采样(转置卷积,也叫反卷积 `Conv2dTranspose2d`等)
3. `Unpooling` 的方法(简单的补零或者扩充操作)
> 计算效果:最近邻插值算法 < 双线性插值 < 双三次插值。计算速度:最近邻插值算法 > 双线性插值 > 双三次插值。
### 5.2,F.interpolate 采样函数
> Pytorch 老版本有 `nn.Upsample` 函数,新版本建议用 `torch.nn.functional.interpolate`,一个函数可实现定制化需求的上采样或者下采样功能,。
`F.interpolate()` 函数全称是 `torch.nn.functional.interpolate()`,函数定义如下:
```python
def interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None): # noqa: F811
# type: (Tensor, Optional[int], Optional[List[float]], str, Optional[bool], Optional[bool]) -> Tensor
pass
```
参数解释如下:
+ `input`(Tensor):输入张量数据;
+ `size`: 输出的尺寸,数据类型为 tuple: ([optional D_out], [optional H_out], W_out),和 `scale_factor` 二选一。
+ `scale_factor`:在高度、宽度和深度上面的放大倍数。数据类型既可以是 int`——表明高度、宽度、深度都扩大同一倍数;也可是 `tuple`——指定高度、宽度、深度等维度的扩大倍数。
+ `mode`: 上采样的方法,包括最近邻(`nearest`),线性插值(`linear`),双线性插值(`bilinear`),三次线性插值(`trilinear`),默认是最近邻(`nearest`)。
+ `align_corners`: 如果设为 `True`,输入图像和输出图像角点的像素将会被对齐(aligned),这只在 `mode = linear, bilinear, or trilinear` 才有效,默认为 `False`。
例子程序如下:
```python
import torch.nn.functional as F
x = torch.rand(1,3,224,224)
y = F.interpolate(x * 2, scale_factor=(2, 2), mode='bilinear').squeeze(0)
print(y.shape) # torch.Size([3, 224, 224)
```
### 5.3 nn.ConvTranspose2d 反卷积
转置卷积(有时候也称为反卷积,个人觉得这种叫法不是很规范),它是一种特殊的卷积,先 `padding` 来扩大图像尺寸,紧接着跟正向卷积一样,旋转卷积核 180 度,再进行卷积计算。
【等待更新】
## 参考资料
+ [pytorch演示卷积和反卷积运算](https://blog.csdn.net/qq_37879432/article/details/80297263)
+ [torch.Tensor](https://pytorch.org/docs/1.7.0/tensors.html#torch.Tensor)
+ [PyTorch学习笔记(10)——上采样和PixelShuffle](https://blog.csdn.net/g11d111/article/details/82855946)
+ [反卷积 Transposed convolution](https://zhuanlan.zhihu.com/p/124626648)
+ [PyTorch中的转置卷积详解——全网最细](https://blog.csdn.net/w55100/article/details/106467776)
+ [4-1,张量的结构操作](https://github.com/lyhue1991/eat_pytorch_in_20_days/blob/master/4-1,%E5%BC%A0%E9%87%8F%E7%9A%84%E7%BB%93%E6%9E%84%E6%93%8D%E4%BD%9C.md)
================================================
FILE: 2-deep_learning_basic/pytorch_basic/src/tensor_demo.py
================================================
import torch
def tensor_learn():
matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],
[[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)
print(matrix) # 打印 tensor
print(matrix.dtype) # 打印 tensor 数据类型
print(matrix.dim()) # 打印 tensor 维度
print(matrix.size()) # 打印 tensor 尺寸
print(matrix.shape) # 打印 tensor 尺寸
matrix2 = matrix.view(4, 2, 2) # 改变 tensor 尺寸
print(matrix2)
print(matrix.numel())
def generate_tensor_and_print_stats(size, func="normal", mean=0.0, std=1.0):
"""
生成一个张量并打印其均值和标准差。
参数:
- size (tuple): 生成张量的形状。
- func (str): 使用的生成函数。选项包括 "normal" 和 "randn"。
- mean (float, 可选): 正态分布的均值,仅在 func="normal" 时使用。默认值为0.0。
- std (float, 可选): 正态分布的标准差,仅在 func="normal" 时使用。默认值为1.0。
返回:
- tensor (Tensor): 生成的张量。
"""
if func == "normal":
tensor = torch.normal(mean=mean, std=std, size=size)
elif func == "randn":
tensor = torch.randn(size=size)
else:
raise ValueError(f"无效的函数类型: {func}. 请使用 'normal' 或 'randn'。")
tensor_mean = tensor.mean().item()
tensor_std = tensor.std().item()
print(f"Tensor Size: {size}, by torch.{func}, Mean: {tensor_mean:.4f}, Std Dev: {tensor_std:.4f}")
return tensor
def main():
tensor_learn()
# 设置随机种子以确保结果可重复(可选)
torch.manual_seed(40)
# 定义不同的张量大小
sizes = [(3, 3), (100, 100), (1000, 1000)]
functions = ["normal", "randn"]
# 生成并打印统计信息
for func in functions:
for size in sizes:
if func == "normal":
generate_tensor_and_print_stats(size=size, func=func, mean=0.0, std=1.0)
elif func == "randn":
generate_tensor_and_print_stats(size=size, func=func)
if __name__ == "__main__":
main()
================================================
FILE: 2-deep_learning_basic/pytorch_basic/src/tensor_math.py
================================================
import torch
# 创建一个 2D 张量
b = torch.tensor([[1, 2, 3], [4, 5, 6]])
print("原始张量:\n", b)
# 沿第 0 维计算累积乘积
cumprod_b_dim0 = torch.cumprod(b, dim=0)
print("沿第 0 维的累积乘积结果:\n", cumprod_b_dim0)
# 沿第 1 维计算累积乘积
cumprod_b_dim1 = torch.cumprod(b, dim=1)
print("沿第 1 维的累积乘积结果:\n", cumprod_b_dim1)
================================================
FILE: 2-deep_learning_basic/pytorch_code/pytorch-c10模块详解.md
================================================
- [一 c10 模块概述](#一-c10-模块概述)
- [二 Stream 类](#二-stream-类)
- [2.1 Stream 抽象类](#21-stream-抽象类)
- [2.2 CUDAStream 类](#22-cudastream-类)
- [2.2 Stream python 类](#22-stream-python-类)
- [三 Event 类](#三-event-类)
- [3.1 Event 类](#31-event-类)
- [四 设备管理工具类-InlineDeviceGuard](#四-设备管理工具类-inlinedeviceguard)
- [参考资料](#参考资料)
## 一 c10 模块概述
`c10` 模块更倾向于提供**高级别的抽象和功能**,而 `ATen`则提供了**更接近硬件的底层操作**。c10 主要模块包括:
+ **c10/core**: 该子目录定义了很多基础类型和核心概念,如 **Device**、**Stream**、**DataPtr**、错误处理(例如 C10_ERROR 系列接口)等。
- `TensorImpl`:Tensor 的底层实现,存储数据指针、形状(sizes)、步长(strides)、设备(device)等元信息。
- `ScalarType`:定义**标量**数据类型(如 kFloat、kInt)。
- **Device**:封装了设备信息(如 CPU、CUDA 等),支持设备间的统一管理。
- **Stream**:提供对异步流(如 CUDA 流)的抽象,允许在不同设备上执行并行操作。
- 其他核心组件还可能包括与内存管理、后端调度相关的工具。
+ c10/cuda/:作用:CUDA 设备管理、流(Stream)、事件(Event)、内存分配等。
- CUDAStream:定义 CUDA 流(cudaStream_t)抽象接口,管理异步 GPU 操作。
- CUDAGuard:设置当前设备(Device)和流(Stream)的上下文守卫。
- CUDAFunctions:CUDA 运行时 API 的封装(如设备同步、内存拷贝)。
- CUDACachingAllocator:CUDA 内存分配器,支持高效的内存池管理。
+ **c10/util**: 包含各种实用的模板和辅助工具,例如 **Optional**、**ArrayRef**、类型特性工具等,这些工具在整个框架中帮助简化数据结构管理和算法实现。
+ **c10/macros**: 提供了大量宏定义,用于统一错误检查、分支预测优化(如`C10_LIKELY/C10_UNLIKELY`)、调试信息输出、编译器平台适配等功能。这些宏在高性能代码路径中起到优化和代码风格统一的作用。
+ **其他模块**:根据版本不同,c10 中可能还会包含与特定后端(例如 `CUDA`、`hip` 相关)的适配代码、日志系统实现、以及与分布式或异步执行有关的工具。
## 二 Stream 类
### 2.1 Stream 抽象类
`Stream` 主要用于表示一个设备(通常是 GPU,比如 CUDA)上的操作队列或执行流。pytorch 中 c10 模块的 `Stream` 抽象定义的接口实现在 `c10/core/Stream.h`文件中。
`Stream` 抽象类定义的公有成员函数如下所示:
<center>
<img src="../../images/pytorch_c10/stream_funcs.png" width="20%" alt="stream_funcs">
</center>
`Stream` 类定义代码如下所示:
```cpp
using StreamId = int64_t;
class C10_API Stream final {
private:
Device device_;
StreamId id_;
public:
enum Unsafe { UNSAFE };
enum Default { DEFAULT };
/// Unsafely construct a stream from a Device and a StreamId. In
/// general, only specific implementations of streams for a
/// backend should manufacture Stream directly in this way; other users
/// should use the provided APIs to get a stream. In particular,
/// we don't require backends to give any guarantees about non-zero
/// StreamIds; they are welcome to allocate in whatever way they like.
explicit Stream(Unsafe, Device device, StreamId id)
: device_(device), id_(id) {}
/// Construct the default stream of a Device. The default stream is
/// NOT the same as the current stream; default stream is a fixed stream
/// that never changes, whereas the current stream may be changed by
/// StreamGuard.
explicit Stream(Default, Device device) : device_(device), id_(0) {}
bool operator==(const Stream& other) const noexcept {
return this->device_ == other.device_ && this->id_ == other.id_;
}
bool operator!=(const Stream& other) const noexcept {
return !(*this == other);
}
Device device() const noexcept {
return device_;
}
DeviceType device_type() const noexcept {
return device_.type();
}
DeviceIndex device_index() const noexcept {
return device_.index();
}
StreamId id() const noexcept {
return id_;
}
// Enqueues a wait instruction in the stream's work queue.
// This instruction is a no-op unless the event is marked
// for recording. In that case the stream stops processing
// until the event is recorded.
template <typename T>
void wait(const T& event) const {
event.block(*this);
}
// Return whether all asynchronous work previously enqueued on this stream
// has completed running on the device.
bool query() const;
// Wait (by blocking the calling thread) until all asynchronous work enqueued
// on this stream has completed running on the device.
void synchronize() const;
// The purpose of this function is to more conveniently permit binding
// of Stream to and from Python. Without packing, I have to setup a whole
// class with two fields (device and stream id); with packing I can just
// store a single uint64_t.
//
// The particular way we pack streams into a uint64_t is considered an
// implementation detail and should not be relied upon.
uint64_t hash() const noexcept {
// Concat these together into a 64-bit integer
uint64_t bits = static_cast<uint64_t>(device_type()) << 56 |
static_cast<uint64_t>(device_index()) << 48 |
// Remove the sign extension part of the 64-bit address because
// the id might be used to hold a pointer.
(static_cast<uint64_t>(id()) & ((1ull << 48) - 1));
return bits;
}
struct StreamData3 pack3() const {
return {id(), device_index(), device_type()};
}
static Stream unpack3(
StreamId stream_id,
DeviceIndex device_index,
DeviceType device_type) {
TORCH_CHECK(isValidDeviceType(device_type));
return Stream(UNSAFE, Device(device_type, device_index), stream_id);
}
// I decided NOT to provide setters on this class, because really,
// why would you change the device of a stream? Just construct
// it correctly from the beginning dude.
};
C10_API std::ostream& operator<<(std::ostream& stream, const Stream& s);
} // namespace c10
```
### 2.2 CUDAStream 类
Cuda API 可分为同步和异步两类,**同步函数会阻塞 host 端的线程执行,异步函数会立刻将控制权返还给 host 从而继续执行之后的动作。**异步函数和 stream 是 grid level 并行的两个基石
CUDAStream 是 PyTorch 管理 CUDA 异步操作的核心类,用于控制 GPU 任务的并行执行和同步。Cuda stream 对象是指一堆异步的 cuda 操作,他们按照 host 代码调用的顺序执行在 device上。Stream 维护了这些操作的顺序,并在所有预处理完成后允许这些操作进入工作队列,同时也可以对这些操作进行一些查询操作。
CUDAStream 类的代码实现在 `c10/cuda/CUDAStream.h` 和 `CUDAStream.cpp` 文件中。私有成员变量定义:
```cpp
class C10_CUDA_API CUDAStream {
public:
enum Unchecked { UNCHECKED };
/// Construct a CUDAStream from a Stream. This construction is checked,
/// and will raise an error if the Stream is not, in fact, a CUDA stream.
explicit CUDAStream(Stream stream) : stream_(stream) {
TORCH_CHECK(stream_.device_type() == DeviceType::CUDA);
}
// 代码省略
private:
Stream stream_; // 公共抽象父类对象,定义在 c10/core/Stream.h
}
```
2, 公有成员函数。
<center>
<img src="../../images/pytorch_c10/cuda_stream_funcs.png" width="20%" alt="cuda_stream_funcs">
</center>
3,`CUDAStream` 流的创建和销毁。
- 默认流:
- 每个设备有一个默认流(Default Stream),通过 `getDefaultCUDAStream` 函数获取默认流对象。
- 默认流是同步流,操作按顺序执行。
```cpp
/**
* Get the default CUDA stream, for the passed CUDA device, or for the
* current device if no device index is passed. The default stream is
* where most computation occurs when you aren't explicitly using
* streams.
*/
C10_API CUDAStream getDefaultCUDAStream(DeviceIndex device_index = -1);
```
- **独立流**:
- 通过 `getStreamFromPool` 从流池中获取,避免频繁创建销毁流的开销。
- 独立流可以并行执行,但需要手动同步。
```cpp
/**
* Get a new stream from the CUDA stream pool. You can think of this
* as "creating" a new stream, but no such creation actually happens;
* instead, streams are preallocated from the pool and returned in a
* round-robin fashion.
*
* You can request a stream from the high priority pool by setting
* isHighPriority to true, or a stream for a specific device by setting device
* (defaulting to the current CUDA stream.)
*/
C10_API CUDAStream
getStreamFromPool(const bool isHighPriority = false, DeviceIndex device = -1);
// no default priority to disambiguate overloads
C10_API CUDAStream
getStreamFromPool(const int priority, DeviceIndex device = -1);
```
4,各个函数实现分析。
```cpp
// 目的:确保当前流的所有 CUDA 操作执行完毕,提供一种同步机制。
void synchronize() const {
// 确保在调用 CUDA 同步操作前,将当前设备切换为流所属的设备
DeviceGuard guard{stream_.device()};
// 调用 c10::cuda::stream_synchronize() 对当前流进行同步,
// 即阻塞直到流中所有操作执行完毕,确保 CUDA 操作已完成
c10::cuda::stream_synchronize(stream());
}
```
### 2.2 Stream python 类
`Stream` 和 `Event` 的 `python` 类代码实现都在[ torch/cuda/streams.py](https://github.com/pytorch/pytorch/blob/v2.6.0/torch/cuda/streams.py#L140),分别继承自 torch._C._CudaStreamBase 和 torch._C._CudaEventBase。
+ `Stream` 类包含 wait_event、wait_stream、record_event、 query 和 synchronize 等成员函数。
+ `Event` 类主要包含:record、wait、query、elapsed_time 和 synchronize 等成员函数。
<center>
<img src="../../images/pytorch_c10/cuda_stream_py_funcs.png" width="20%" alt="cuda_stream_py_funcs">
</center>
## 三 Event 类
### 3.1 Event 类
1,什么是 Event
Event 是 stream 相关的一个重要设计,用于在 CUDA Stream 中插入事件,**记录 GPU 操作的时间戳或实现流间同步**。`Event` 类的关键方法:
- `record(stream=None)`:在指定流中记录事件。
- `synchronize()`:阻塞 CPU 主机线程,直到直到此事件中当前捕获的所有工作完成。
- `wait(stream)`:让提交到给定 stream 的所有未来工作等待此事件。如果未指定流,则使用 `torch.cuda.current_stream()`。
- `elapsed_time(end_event)`:计算两个事件的时间差(毫秒)。
- `query()`: 提供了非阻塞式的完成状态检查机制。
2,Event 的生命周期
- **创建**(cudaEventCreate):调用 cudaEventCreate() 或 cudaEventCreateWithFlags() 创建一个 Event 对象。
- **记录**(cudaEventRecord):将 Event 推入指定的 stream。如果你使用默认流(即 0 或 cudaStreamDefault),则该 Event 会在所有流上顺序生效(见下文默认流语义)。
- **查询**(cudaEventQuery):异步查询该 Event 是否已完成。如果为 “未完成”,函数会立即返回 cudaErrorNotReady;只有当该 Event 标记的 stream 中位于它之前的所有操作都执行完毕,query() 才会返回 cudaSuccess。
- 等待/同步(cudaEventSynchronize):阻塞当前 CPU 线程,直到该 Event 完成。
- 销毁(cudaEventDestroy):释放 Event 资源。
下图是是 c10/core/Event.h 中 Event 类的成员函数总结,各个函数的实现其实是在 c10/core/impl/InlineEvent.h 中。
<center>
<img src="../../images/pytorch_c10/event_funcs.png" width="20%" alt="event_funcs">
</center>
`c10/core/impl/InlineEvent.h` 文件中 `InlineEvent` 类的总结和注释如下所示:
```cpp
// InlineEvent 是一个模板结构体,用于封装事件(Event)的操作,其行为依赖于具体的后端实现(通过模板参数 T 提供)。
// 注意:构造函数被删除,必须传入设备类型和可选的 EventFlag 进行初始化。
template <typename T>
struct InlineEvent final {
// 禁用默认构造函数
InlineEvent() = delete;
// 构造函数:需要传入设备类型和可选的标志参数(默认为 PYTORCH_DEFAULT)
InlineEvent(
const DeviceType _device_type,
const EventFlag _flag = EventFlag::PYTORCH_DEFAULT)
: backend_{_device_type}, // 利用设备类型初始化后端实现对象
device_type_{_device_type}, // 记录事件所属的设备类型
flag_{_flag} {} // 记录事件标志
// 禁用拷贝构造和拷贝赋值,避免误拷贝事件对象
InlineEvent(const InlineEvent&) = delete;
InlineEvent& operator=(const InlineEvent&) = delete;
// 移动构造函数,允许事件对象在移动语义下转移所有权
InlineEvent(InlineEvent&& other) noexcept
: event_(other.event_), // 传递底层事件指针
backend_(std::move(other.backend_)), // 移动后端实现对象
device_type_(other.device_type_), // 拷贝设备类型
device_index_(other.device_index_), // 拷贝设备索引
flag_(other.flag_), // 拷贝事件标志
was_marked_for_recording_(other.was_marked_for_recording_) {
// 移动后将原对象的事件指针置空,避免重复释放
other.event_ = nullptr;
}
// 移动赋值运算符,利用 swap 实现
InlineEvent& operator=(InlineEvent&& other) noexcept {
swap(other);
return *this;
}
// 交换两个 InlineEvent 对象的所有成员
void swap(InlineEvent& other) noexcept {
std::swap(event_, other.event_);
std::swap(backend_, other.backend_);
std::swap(device_type_, other.device_type_);
std::swap(device_index_, other.device_index_);
std::swap(flag_, other.flag_);
std::swap(was_marked_for_recording_, other.was_marked_for_recording_);
}
// 析构函数:若 event_ 不为空,则通过后端对象释放资源
~InlineEvent() noexcept {
if (event_)
backend_.destroyEvent(event_, device_index_);
}
// 返回事件所属设备的类型
DeviceType device_type() const noexcept {
return device_type_;
}
// 返回事件所属设备的索引
DeviceIndex device_index() const noexcept {
return device_index_;
}
// 返回事件的标志
EventFlag flag() const noexcept {
return flag_;
}
// 查询事件是否已被记录
bool was_marked_for_recording() const noexcept {
return was_marked_for_recording_;
}
// 如果尚未记录,则记录一次当前流
void recordOnce(co
gitextract_72z1ypi7/ ├── .gitignore ├── 1-math_ml_basic/ │ ├── cpp_learn_xmind/ │ │ └── C++多线程-并发编程.xmind │ ├── grpc基础笔记.md │ ├── nlp背景知识总结.md │ ├── python_learn_xmind/ │ │ ├── Python 编程笔记-面向对象高级编程.xmind │ │ ├── Python数据结构和高级特性.xmind │ │ ├── Python编程笔记- 函数式编程.xmind │ │ └── Python编程笔记-面向对象基础编程.xmind │ ├── rust编程基础.md │ ├── src/ │ │ ├── cpp/ │ │ │ ├── binary_search.cpp │ │ │ ├── class_copy_move.cpp │ │ │ ├── cpp_basic.cpp │ │ │ ├── fileWrapper.cpp │ │ │ ├── multi_thread_demo.cpp │ │ │ └── test_template.cpp │ │ └── python/ │ │ ├── chatgpt_api_demo.py │ │ ├── classifynet_torch_to_onnx.py │ │ ├── conv_layer.py │ │ ├── gd_double_variable.py │ │ ├── gradio_demo.py │ │ ├── multi_cal_tasks.py │ │ ├── multi_io_tasks.py │ │ ├── python_basic.py │ │ └── thop_demo.py │ ├── ssh远程登录服务.md │ ├── transformers库快速入门.md │ ├── 深度学习基础-机器学习基本原理.md │ ├── 深度学习数学基础-概率与信息论.md │ └── 随机梯度下降法的数学基础.md ├── 2-deep_learning_basic/ │ ├── cnn基础部件-BN层详解.md │ ├── cnn基础部件-卷积层详解.md │ ├── cnn基础部件-激活函数详解.md │ ├── pytorch_basic/ │ │ ├── Pytorch基础-tensor数据结构.md │ │ ├── Pytorch基础-张量数学运算.md │ │ ├── Pytorch基础-张量结构操作.md │ │ └── src/ │ │ ├── tensor_demo.py │ │ └── tensor_math.py │ ├── pytorch_code/ │ │ ├── pytorch-c10模块详解.md │ │ ├── pytorch代码库结构拆解.md │ │ ├── pytorch张量实现分析.md │ │ ├── pytorch架构概览.md │ │ └── pytorch编译流程解析.md │ ├── 反向传播与梯度下降详解.md │ ├── 深度学习基础-优化算法详解.md │ ├── 深度学习基础-参数初始化详解.md │ ├── 深度学习基础-损失函数详解.md │ └── 深度学习基础总结.md ├── 3-classic_backbone/ │ ├── DenseNet论文解读.md │ ├── ResNetv2论文解读.md │ ├── ResNet网络详解.md │ ├── densenet.py │ ├── efficient_cnn/ │ │ ├── CSPNet论文详解.md │ │ ├── MobileNetv1论文详解.md │ │ ├── RepVGG论文详解.md │ │ ├── ShuffleNetv2论文详解.md │ │ ├── VoVNet论文解读.md │ │ └── vovnet.py │ ├── shufflenetv2_expr.py │ └── 经典backbone总结.md ├── 4-deep_learning_alchemy/ │ ├── 深度学习炼丹-不平衡样本的处理.md │ ├── 深度学习炼丹-数据增强.md │ ├── 深度学习炼丹-数据标准化.md │ ├── 深度学习炼丹-模型可视化.md │ ├── 深度学习炼丹-正则化策略.md │ └── 深度学习炼丹-超参数调整.md ├── 5-model_compression/ │ ├── README.md │ ├── 基于pytorch实现模型剪枝.md │ ├── 模型压缩-剪枝算法详解.md │ ├── 模型压缩-知识蒸馏详解.md │ ├── 模型压缩-神经网络量化基础.md │ ├── 模型压缩-轻量化网络总结.md │ └── 深度学习模型压缩方法概述.md ├── 6-model_deploy/ │ ├── AI芯片速览.md │ ├── ONNX模型分析与使用.md │ ├── TensorRT基础笔记.md │ ├── ncnn源码解析-Net类.md │ ├── ncnn源码解析-sample运行.md │ ├── 卷积神经网络复杂度分析.md │ ├── 模型压缩部署概述.md │ └── 模型推理加速技巧-融合卷积和BN层.md ├── LICENSE ├── README.md ├── images/ │ └── dl/ │ └── courgette.log ├── process_image.py ├── 互联网技术大佬独立博客推荐.md └── 手把手教你注册和使用ChatGPT.md
SYMBOL INDEX (157 symbols across 18 files)
FILE: 1-math_ml_basic/src/cpp/binary_search.cpp
function binary_search (line 7) | int binary_search(std::vector<int> arr, int target) {
function main (line 23) | int main() {
FILE: 1-math_ml_basic/src/cpp/class_copy_move.cpp
class Complex (line 3) | class Complex
method Complex (line 6) | Complex(double r = 0, double i = 0) : real(r), imag(i) {}
method Complex (line 11) | Complex operator+(const Complex& other) const
method Complex (line 17) | Complex operator-(const Complex& other) const
function main (line 32) | int main()
FILE: 1-math_ml_basic/src/cpp/cpp_basic.cpp
class MyClass (line 5) | class MyClass {
method MyClass (line 7) | MyClass() { std::cout << "Default constructor" << std::endl; }
method MyClass (line 8) | MyClass(const MyClass& other) { std::cout << "Copy constructor" << std...
method MyClass (line 9) | MyClass(MyClass&& other) { std::cout << "Move constructor" << std::end...
function main (line 13) | int main() {
FILE: 1-math_ml_basic/src/cpp/fileWrapper.cpp
class fileWrapper (line 6) | class fileWrapper {
method fileWrapper (line 9) | explicit fileWrapper(const std::string& filename) :
method writeLine (line 24) | void writeLine(const std::string& line) {
function wrapper (line 36) | void wrapper(T&& val) {
function main (line 41) | int main() {
FILE: 1-math_ml_basic/src/cpp/multi_thread_demo.cpp
function worker (line 9) | void worker() {
function simple_do_once (line 14) | void simple_do_once(std::once_flag* flag)
function main (line 19) | int main() {
FILE: 1-math_ml_basic/src/cpp/test_template.cpp
type Device (line 10) | enum class Device {
class Tensor (line 18) | class Tensor
class Tensor<Device::CPU, T> (line 23) | class Tensor<Device::CPU, T> {
method Tensor (line 26) | explicit Tensor(const vector<size_t> shape): shape(shape) {
method T (line 35) | const T& operator[](size_t index) const {
method size (line 39) | size_t size() const noexcept {
method add (line 45) | void add(const Tensor<Device::CPU, T> input_t) {
method printDebug (line 53) | void printDebug(const std::string& name) const {
function func (line 67) | auto func(const T1& x, const T2& y) {
function func (line 73) | auto func(const int& x, const double& y) {
function main (line 77) | int main() {
FILE: 1-math_ml_basic/src/python/classifynet_torch_to_onnx.py
function arg_parse (line 29) | def arg_parse():
function model_inference (line 38) | def model_inference(net, input_data, weight_path):
function torch_to_onnx (line 56) | def torch_to_onnx(torch_model, input_data, onnx_file_path):
function define_net (line 67) | def define_net():
function main (line 71) | def main(net_torch, net_name, input_shape):
FILE: 1-math_ml_basic/src/python/conv_layer.py
class Conv2D (line 10) | class Conv2D:
method __init__ (line 11) | def __init__(self, input_channels, output_channels, kernel_size):
method forward (line 18) | def forward(self, input):
method __init__ (line 64) | def __init__(self, input_channels, output_channels, kernel_size):
method forward (line 68) | def forward(self, x):
class Conv2D (line 63) | class Conv2D(torch.nn.Module):
method __init__ (line 11) | def __init__(self, input_channels, output_channels, kernel_size):
method forward (line 18) | def forward(self, input):
method __init__ (line 64) | def __init__(self, input_channels, output_channels, kernel_size):
method forward (line 68) | def forward(self, x):
FILE: 1-math_ml_basic/src/python/gd_double_variable.py
function target_function (line 8) | def target_function(x,y):
function derivative_function (line 12) | def derivative_function(theta):
function show_3d_surface (line 17) | def show_3d_surface(x, y, z):
FILE: 1-math_ml_basic/src/python/gradio_demo.py
function user (line 10) | def user(user_message, history):
function bot (line 13) | def bot(history):
FILE: 1-math_ml_basic/src/python/multi_cal_tasks.py
function timer (line 15) | def timer(func):
function matrix_mult (line 26) | def matrix_mult(A, B):
function matrix_mult_threaded (line 37) | def matrix_mult_threaded(A, B, num_threads):
function matrix_mult_multiprocess (line 57) | def matrix_mult_multiprocess(A, B, num_processes):
FILE: 1-math_ml_basic/src/python/multi_io_tasks.py
function timer (line 34) | def timer(func):
function create_dir (line 45) | def create_dir(dir_name):
function download_image (line 50) | def download_image(url, save_path):
function single_thread_download (line 57) | def single_thread_download():
function multi_thread_download (line 65) | def multi_thread_download():
function thread_worker (line 81) | def thread_worker(work_queue, dir_name):
function thread_pool_download (line 94) | def thread_pool_download():
function process_worker (line 114) | def process_worker(url):
function process_pool_download (line 119) | def process_pool_download():
FILE: 1-math_ml_basic/src/python/python_basic.py
function fibonacci (line 2) | def fibonacci():
function timer (line 31) | def timer(func):
function runTime (line 41) | def runTime(func):
function fib (line 55) | def fib(n):
function cache (line 69) | def cache(func):
function fibonacci (line 85) | def fibonacci(n):
function my_decorator (line 94) | def my_decorator(func):
function example_function (line 103) | def example_function(x, y):
class Shape (line 111) | class Shape:
method area (line 112) | def area(self):
method perimeter (line 115) | def perimeter(self):
class Rectangle (line 118) | class Rectangle(Shape):
method __init__ (line 119) | def __init__(self, width, height):
method area (line 124) | def area(self):
method perimeter (line 127) | def perimeter(self):
class Circle (line 130) | class Circle(Shape):
method __init__ (line 131) | def __init__(self, radius):
method area (line 135) | def area(self):
method perimeter (line 138) | def perimeter(self):
function calculate (line 141) | def calculate(shape):
class Person (line 163) | class Person:
method __init__ (line 164) | def __init__(self, age):
method age (line 169) | def age(self):
method age (line 173) | def age(self, value):
method age_group (line 183) | def age_group(self):
method _calculate_age_group (line 186) | def _calculate_age_group(self):
class A (line 204) | class A:
method method1 (line 205) | def method1(self):
class B (line 208) | class B:
method method1 (line 209) | def method1(self):
method method2 (line 211) | def method2(self):
class C (line 214) | class C(A, B):
method method3 (line 215) | def method3(self):
class IntList (line 230) | class IntList:
method __init__ (line 231) | def __init__(self, data):
method __len__ (line 234) | def __len__(self):
method __getitem__ (line 237) | def __getitem__(self, index):
method __setitem__ (line 243) | def __setitem__(self, index, value):
method __delitem__ (line 249) | def __delitem__(self, index):
method __contains__ (line 255) | def __contains__(self, item):
method __iter__ (line 258) | def __iter__(self):
method __repr__ (line 261) | def __repr__(self):
class Meta (line 281) | class Meta(type):
method __init__ (line 282) | def __init__(cls, name, bases, attrs):
class MyClass (line 286) | class MyClass(metaclass=Meta):
method name (line 288) | def name(self):
class StringUtils (line 296) | class StringUtils:
method reverse_string (line 298) | def reverse_string(string):
method count_characters (line 303) | def count_characters(cls, string):
method __init__ (line 307) | def __init__(self, string):
method reverse_instance_string (line 310) | def reverse_instance_string(self):
class Person (line 331) | class Person:
method __init__ (line 164) | def __init__(self, age):
method age (line 169) | def age(self):
method age (line 173) | def age(self, value):
method age_group (line 183) | def age_group(self):
method _calculate_age_group (line 186) | def _calculate_age_group(self):
class Student (line 348) | class Student:
method __init__ (line 349) | def __init__(self, age):
method __lt__ (line 352) | def __lt__(self, other):
method __eq__ (line 358) | def __eq__(self, other):
function get_dict_depth (line 374) | def get_dict_depth(d, depth=0):
FILE: 2-deep_learning_basic/pytorch_basic/src/tensor_demo.py
function tensor_learn (line 3) | def tensor_learn():
function generate_tensor_and_print_stats (line 15) | def generate_tensor_and_print_stats(size, func="normal", mean=0.0, std=1...
function main (line 41) | def main():
FILE: 3-classic_backbone/densenet.py
function _bn_function_factory (line 12) | def _bn_function_factory(norm, relu, conv):
class _DenseLayer (line 21) | class _DenseLayer(nn.Module):
method __init__ (line 22) | def __init__(self, num_input_features, growth_rate, bn_size, drop_rate...
method forward (line 35) | def forward(self, *prev_features):
class _Transition (line 47) | class _Transition(nn.Sequential):
method __init__ (line 48) | def __init__(self, num_input_features, num_output_features):
class _DenseBlock (line 57) | class _DenseBlock(nn.Module):
method __init__ (line 58) | def __init__(self, num_layers, num_input_features, bn_size, growth_rat...
method forward (line 70) | def forward(self, init_features):
class DenseNet (line 78) | class DenseNet(nn.Module):
method __init__ (line 92) | def __init__(self, growth_rate=12, block_config=(16, 16, 16), compress...
method forward (line 150) | def forward(self, x):
FILE: 3-classic_backbone/efficient_cnn/vovnet.py
function conv3x3 (line 16) | def conv3x3(in_channels, out_channels, module_name, postfix,
function conv1x1 (line 34) | def conv1x1(in_channels, out_channels, module_name, postfix,
class _OSA_module (line 52) | class _OSA_module(nn.Module):
method __init__ (line 53) | def __init__(self,
method forward (line 75) | def forward(self, x):
class _OSA_stage (line 92) | class _OSA_stage(nn.Sequential):
method __init__ (line 93) | def __init__(self,
class VoVNet (line 124) | class VoVNet(nn.Module):
method __init__ (line 125) | def __init__(self,
method forward (line 164) | def forward(self, x):
function _vovnet (line 173) | def _vovnet(arch,
function vovnet57 (line 191) | def vovnet57(pretrained=False, progress=True, **kwargs):
function vovnet39 (line 203) | def vovnet39(pretrained=False, progress=True, **kwargs):
function vovnet27_slim (line 215) | def vovnet27_slim(pretrained=False, progress=True, **kwargs):
FILE: 3-classic_backbone/shufflenetv2_expr.py
function benchmark (line 6) | def benchmark(model, input_tensor, batch_size, device, num_iterations=100):
class ConvBlock (line 28) | class ConvBlock(nn.Module):
method __init__ (line 29) | def __init__(self, c1, c2):
method forward (line 35) | def forward(self, x):
class ConvNet (line 41) | class ConvNet(nn.Module):
method __init__ (line 42) | def __init__(self, c1, c2, num_blocks=10):
method forward (line 46) | def forward(self, x):
FILE: process_image.py
function contains_chinese (line 14) | def contains_chinese(text):
function translate_text (line 18) | def translate_text(text):
function get_new_filename (line 30) | def get_new_filename(original_name, extension, existing_names):
function main (line 40) | def main():
Condensed preview — 86 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,000K chars).
[
{
"path": ".gitignore",
"chars": 9,
"preview": ".DS_Store"
},
{
"path": "1-math_ml_basic/grpc基础笔记.md",
"chars": 13626,
"preview": "## 一,ProtoBuf 基础\n\n在网络通信和通用数据交换等应用场景中经常使用的技术除了 `JSON` 和 `XML`,另外一个就是 `ProtoBuf`。protocol buffers (ProtoBuf)是一种语言无关、平台无关、可"
},
{
"path": "1-math_ml_basic/nlp背景知识总结.md",
"chars": 6345,
"preview": "- [词向量](#词向量)\n - [`One-Hot` 编码](#one-hot-编码)\n - [Word Embedding](#word-embedding)\n- [vocab 和 merge table](#vocab-和-mer"
},
{
"path": "1-math_ml_basic/rust编程基础.md",
"chars": 3987,
"preview": "## 认识 Cargo\n\n`cargo` 是 `Rust` 的包管理工具,提供了从项目的建立、构建到测试、运行乃至部署的完整功能,与 `Rust` 语言及其编译器 `rustc` 紧密结合。\n\n1,创建项目。\n\n```bash\n$ carg"
},
{
"path": "1-math_ml_basic/src/cpp/binary_search.cpp",
"chars": 628,
"preview": "#include <stdio.h>\n#include <iostream>\n#include <vector>\n\nusing namespace std;\n\nint binary_search(std::vector<int> arr, "
},
{
"path": "1-math_ml_basic/src/cpp/class_copy_move.cpp",
"chars": 977,
"preview": "#include <iostream>\n\nclass Complex\n{\npublic:\n Complex(double r = 0, double i = 0) : real(r), imag(i) {}\n\n // 重载 + "
},
{
"path": "1-math_ml_basic/src/cpp/cpp_basic.cpp",
"chars": 839,
"preview": "#include <iostream>\n#include <vector>\n#include <string>\n\nclass MyClass {\npublic:\n MyClass() { std::cout << \"Default c"
},
{
"path": "1-math_ml_basic/src/cpp/fileWrapper.cpp",
"chars": 1272,
"preview": "#include <stdio.h>\n#include <iostream>\n#include <fstream>\n#include <string>\n\nclass fileWrapper {\npublic:\n // 构造函数 打开文"
},
{
"path": "1-math_ml_basic/src/cpp/multi_thread_demo.cpp",
"chars": 566,
"preview": "#include <stdio.h>\n#include <iostream>\n#include <thread>\n#include <mutex>\n\nusing namespace std;\n\n// 线程函数\nvoid worker() {"
},
{
"path": "1-math_ml_basic/src/cpp/test_template.cpp",
"chars": 1889,
"preview": "#include <iostream>\n#include <vector>\n#include <stdexcept>\n#include <string>\n#include <cassert>\n\nusing namespace std;\n\n/"
},
{
"path": "1-math_ml_basic/src/python/chatgpt_api_demo.py",
"chars": 450,
"preview": "import openai\n\n# After set the API key, the code can run!\nopenai.api_key = \"YOUR_API_KEY\"\n\n# Define the model and prompt"
},
{
"path": "1-math_ml_basic/src/python/classifynet_torch_to_onnx.py",
"chars": 5447,
"preview": "# -*- coding : utf-8 -*-\r\n# author : honggao.zhang\r\n# Create : 2021-2-20\r\n# Update : 2021-3-12\r\n# Versio"
},
{
"path": "1-math_ml_basic/src/python/conv_layer.py",
"chars": 3005,
"preview": "# -*- coding : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n\nimport torch\nimport time\n\nimport numpy as np\nimport time\n\nc"
},
{
"path": "1-math_ml_basic/src/python/gd_double_variable.py",
"chars": 1453,
"preview": "# Copyright (c) Microsoft. All rights reserved.\n# Licensed under the MIT license. See LICENSE file in the project root f"
},
{
"path": "1-math_ml_basic/src/python/gradio_demo.py",
"chars": 612,
"preview": "import gradio as gr\nimport random\nimport time\n\nwith gr.Blocks() as demo:\n chatbot = gr.Chatbot()\n msg = gr.Textbox"
},
{
"path": "1-math_ml_basic/src/python/multi_cal_tasks.py",
"chars": 2921,
"preview": "# -*- coding : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n# Create: 2023-03-07\n# Version : 0.1.0\n# Description: 矩阵"
},
{
"path": "1-math_ml_basic/src/python/multi_io_tasks.py",
"chars": 3796,
"preview": "# -*- coding : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n# Create: 2023-03-07\n# Version : 0.1.0\n# Description: 下载"
},
{
"path": "1-math_ml_basic/src/python/python_basic.py",
"chars": 9155,
"preview": "# 优点1: 生成器可以用于生成无限序列,而列表生成式只能用于有限序列。\ndef fibonacci():\n prev, curr = 0, 1\n while True:\n yield curr\n p"
},
{
"path": "1-math_ml_basic/src/python/thop_demo.py",
"chars": 236,
"preview": "import torch\nfrom torchvision.models import resnet50\nfrom thop import profile\nmodel = resnet50()\ninput = torch.randn(1, "
},
{
"path": "1-math_ml_basic/ssh远程登录服务.md",
"chars": 5181,
"preview": "`SSH`(安全外壳协议 Secure Shell Protocol,简称SSH)是一种加密的网络传输协议,用于在网络中实现客户端和服务端的连接,典型的如我们在本地电脑通过 `SSH`连接远程服务器,从而做开发,Windows、macOS、"
},
{
"path": "1-math_ml_basic/transformers库快速入门.md",
"chars": 11523,
"preview": "- [一,Transformers 术语](#一transformers-术语)\n - [1.1,token、tokenization 和 tokenizer](#11tokentokenization-和-tokenizer)\n - "
},
{
"path": "1-math_ml_basic/深度学习基础-机器学习基本原理.md",
"chars": 12327,
"preview": "---\nlayout: post\ntitle: 深度学习基础-机器学习基本原理\ndate: 2022-11-04 23:00:00\nsummary: 深度学习是机器学习的一个特定分支。我们要想充分理解深度学习,必须对机器学习的基本原理有深刻"
},
{
"path": "1-math_ml_basic/深度学习数学基础-概率与信息论.md",
"chars": 17177,
"preview": "---\nlayout: post\ntitle: 深度学习数学基础-概率与信息论\ndate: 2022-11-01 23:00:00\nsummary: 概率论是用于表示不确定性声明的数学框架。它不仅提供了量化不确定性的方法,也提供了用于导出新"
},
{
"path": "1-math_ml_basic/随机梯度下降法的数学基础.md",
"chars": 4175,
"preview": "---\nlayout: post\ntitle: 随机梯度下降法的数学基础\ndate: 2023-01-20 23:00:00\nsummary: 本文从导数开始讲起,讲述了导数、偏导数、方向导数和梯度的定义、意义和数学公式,有助于初学者后续更"
},
{
"path": "2-deep_learning_basic/cnn基础部件-BN层详解.md",
"chars": 9645,
"preview": "- [一,数学基础](#一数学基础)\n - [1.1,概率密度函数](#11概率密度函数)\n - [1.2,正态分布](#12正态分布)\n- [二,背景](#二背景)\n - [2.1,如何理解 Internal Covariate S"
},
{
"path": "2-deep_learning_basic/cnn基础部件-卷积层详解.md",
"chars": 15840,
"preview": "---\nlayout: post\ntitle: cnn 基础部件-卷积层详解\ndate: 2022-12-15 22:00:00\nsummary: 卷积神经网络核心网络层是卷积层,其使用了卷积(convolution)这种数学运算,卷积是一"
},
{
"path": "2-deep_learning_basic/cnn基础部件-激活函数详解.md",
"chars": 12055,
"preview": "---\nlayout: post\ntitle: cnn 基础部件-激活函数详解\ndate: 2022-12-05 22:00:00\nsummary: 本文分析了激活函数对于神经网络的必要性,同时讲解了几种常见的激活函数的原理,并给出相关公式"
},
{
"path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-tensor数据结构.md",
"chars": 15407,
"preview": "---\nlayout: post\ntitle: Pytorch基础-tensor数据结构\ndate: 2021-03-07 12:00:00\nsummary: torch.Tensor 是一种包含单一数据类型元素的多维矩阵,类似于 nump"
},
{
"path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-张量数学运算.md",
"chars": 7603,
"preview": "---\nlayout: post\ntitle: Pytorch基础-张量数学运算\ndate: 2021-03-03 12:00:00\nsummary: PyTorch 张量数学运算就是对张量的元素值完成数学运算,常用的张量数学运算包括:标量"
},
{
"path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-张量结构操作.md",
"chars": 13519,
"preview": "- [一,张量的基本操作](#一张量的基本操作)\n - [1.1 改变形状: view 和 reshape](#11-改变形状-view-和-reshape)\n - [1.2 张量拼接](#12-张量拼接)\n- [二,维度变换](#二维"
},
{
"path": "2-deep_learning_basic/pytorch_basic/src/tensor_demo.py",
"chars": 1798,
"preview": "import torch\n\ndef tensor_learn():\n matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],\n [[5,4,6,7], ["
},
{
"path": "2-deep_learning_basic/pytorch_basic/src/tensor_math.py",
"chars": 285,
"preview": "import torch\n\n# 创建一个 2D 张量\nb = torch.tensor([[1, 2, 3], [4, 5, 6]])\nprint(\"原始张量:\\n\", b)\n\n# 沿第 0 维计算累积乘积\ncumprod_b_dim0 ="
},
{
"path": "2-deep_learning_basic/pytorch_code/pytorch-c10模块详解.md",
"chars": 14555,
"preview": "- [一 c10 模块概述](#一-c10-模块概述)\n- [二 Stream 类](#二-stream-类)\n - [2.1 Stream 抽象类](#21-stream-抽象类)\n - [2.2 CUDAStream 类](#22-"
},
{
"path": "2-deep_learning_basic/pytorch_code/pytorch代码库结构拆解.md",
"chars": 4488,
"preview": "---\nlayout: post\ntitle: Pytorch 代码库结构拆解\ndate: 2025-03-28 19:00:00\nsummary: pytorch 代码库结构拆解,以及核心目录的功能概述。\ncategories: Fram"
},
{
"path": "2-deep_learning_basic/pytorch_code/pytorch张量实现分析.md",
"chars": 4651,
"preview": "---\nlayout: post\ntitle: Pytorch 张量实现分析\ndate: 2025-03-29 19:00:00\nsummary: pytorch 张量的属性、底层实现分析以及应用,内容持续更新中。\ncategories: "
},
{
"path": "2-deep_learning_basic/pytorch_code/pytorch架构概览.md",
"chars": 13093,
"preview": "- [一 pytorch 框架概述](#一-pytorch-框架概述)\n - [1.1 pytorch 概述](#11-pytorch-概述)\n - [1.2 pytorch 前后端](#12-pytorch-前后端)\n - [1.3"
},
{
"path": "2-deep_learning_basic/pytorch_code/pytorch编译流程解析.md",
"chars": 22687,
"preview": "- [1. 解析 setup.py 中 main 函数流程](#1-解析-setuppy-中-main-函数流程)\n- [2. configure\\_extension\\_build 函数流程](#2-configure_extension"
},
{
"path": "2-deep_learning_basic/反向传播与梯度下降详解.md",
"chars": 8026,
"preview": "- [一,前向传播与反向传播](#一前向传播与反向传播)\n - [1.1,神经网络训练过程](#11神经网络训练过程)\n - [1.2,前向传播](#12前向传播)\n - [1.3,反向传播](#13反向传播)\n - [1.4,总结"
},
{
"path": "2-deep_learning_basic/深度学习基础-优化算法详解.md",
"chars": 10793,
"preview": "## 目录\n- [目录](#目录)\n- [前言](#前言)\n- [一,梯度下降优化算法](#一梯度下降优化算法)\n - [1.1,随机梯度下降 SGD](#11随机梯度下降-sgd)\n - [1.2,动量 Momentum](#12动量"
},
{
"path": "2-deep_learning_basic/深度学习基础-参数初始化详解.md",
"chars": 5624,
"preview": "- [一,参数初始化概述](#一参数初始化概述)\n - [1.1,进行网络参数初始化的原因](#11进行网络参数初始化的原因)\n - [1.2,网络参数初始化为什么重要](#12网络参数初始化为什么重要)\n- [二,权重初始化方式分类]"
},
{
"path": "2-deep_learning_basic/深度学习基础-损失函数详解.md",
"chars": 13355,
"preview": "- [一,损失函数概述](#一损失函数概述)\n- [二,交叉熵函数-分类损失](#二交叉熵函数-分类损失)\n - [2.1,交叉熵(Cross-Entropy)的由来](#21交叉熵cross-entropy的由来)\n - [2.1"
},
{
"path": "2-deep_learning_basic/深度学习基础总结.md",
"chars": 37220,
"preview": "---\nlayout: post\ntitle: 深度学习基础总结\ndate: 2021-10-10 12:00:00\nsummary: 深度学习基础知识总结。\ncategories: DeepLearning\n---\n\n\n- [一,滤波器与"
},
{
"path": "3-classic_backbone/DenseNet论文解读.md",
"chars": 11060,
"preview": "## 目录\n- [目录](#目录)\n- [摘要](#摘要)\n- [网络结构](#网络结构)\n- [优点](#优点)\n- [代码](#代码)\n- [问题](#问题)\n- [参考资料](#参考资料)\n\n## 摘要\n\n`ResNet` 的工作表面"
},
{
"path": "3-classic_backbone/ResNetv2论文解读.md",
"chars": 6441,
"preview": "- [前言](#前言)\n- [摘要](#摘要)\n- [1、介绍](#1介绍)\n- [2、深度残差网络的分析](#2深度残差网络的分析)\n- [3、On the Importance of Identity Skip Connection]("
},
{
"path": "3-classic_backbone/ResNet网络详解.md",
"chars": 4249,
"preview": "## 摘要\n\n残差网络(`ResNet`)的提出是为了**解决深度神经网络的“退化”(优化)问题**。\n\n有[论文](https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1702.0"
},
{
"path": "3-classic_backbone/densenet.py",
"chars": 7098,
"preview": "# This implementation is based on the DenseNet-BC implementation in torchvision\n# https://github.com/pytorch/vision/blob"
},
{
"path": "3-classic_backbone/efficient_cnn/CSPNet论文详解.md",
"chars": 7664,
"preview": "- [摘要](#摘要)\n- [1,介绍](#1介绍)\n- [2,相关工作](#2相关工作)\n- [3,改进方法](#3改进方法)\n - [3.1,Cross Stage Partial Network](#31cross-stage-pa"
},
{
"path": "3-classic_backbone/efficient_cnn/MobileNetv1论文详解.md",
"chars": 17432,
"preview": "- [1、相关工作](#1相关工作)\n - [标准卷积](#标准卷积)\n - [分组卷积](#分组卷积)\n - [DW 卷积](#dw-卷积)\n - [从 Inception module 到 depthwise separable"
},
{
"path": "3-classic_backbone/efficient_cnn/RepVGG论文详解.md",
"chars": 14965,
"preview": "- [背景知识](#背景知识)\n - [VGG 和 ResNet 回顾](#vgg-和-resnet-回顾)\n - [MAC 计算](#mac-计算)\n - [卷积运算与矩阵乘积](#卷积运算与矩阵乘积)\n - [点积](#点积"
},
{
"path": "3-classic_backbone/efficient_cnn/ShuffleNetv2论文详解.md",
"chars": 16059,
"preview": "- [摘要](#摘要)\n- [1、介绍](#1介绍)\n- [2、高效网络设计的实用指导思想](#2高效网络设计的实用指导思想)\n - [G1-同样大小的通道数可以最小化 1x1 卷积 MAC](#g1-同样大小的通道数可以最小化-1x1-"
},
{
"path": "3-classic_backbone/efficient_cnn/VoVNet论文解读.md",
"chars": 18273,
"preview": "- [摘要](#摘要)\n- [1,介绍](#1介绍)\n- [2,高效网络设计的影响因素](#2高效网络设计的影响因素)\n - [2.1,内存访问代价](#21内存访问代价)\n - [2.2,GPU计算效率](#22gpu计算效率)\n- "
},
{
"path": "3-classic_backbone/efficient_cnn/vovnet.py",
"chars": 7795,
"preview": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom collections import OrderedDict\n\n\n__all__ = ['VoV"
},
{
"path": "3-classic_backbone/shufflenetv2_expr.py",
"chars": 2098,
"preview": "import torch\nimport torch.nn as nn\nimport time\n\n# 生成随机数据进行测试\ndef benchmark(model, input_tensor, batch_size, device, num_"
},
{
"path": "3-classic_backbone/经典backbone总结.md",
"chars": 3878,
"preview": "## 目录\n- [目录](#目录)\n- [VGG](#vgg)\n- [ResNet](#resnet)\n- [Inceptionv3](#inceptionv3)\n- [Resnetv2](#resnetv2)\n- [ResNeXt](#r"
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-不平衡样本的处理.md",
"chars": 6239,
"preview": "- [前言](#前言)\n- [一,数据层面处理方法](#一数据层面处理方法)\n - [1.1,数据扩充](#11数据扩充)\n - [1.2,数据(重)采样](#12数据重采样)\n - [数据采样方法总结](#数据采样方法总结)\n "
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-数据增强.md",
"chars": 7721,
"preview": "- [一,数据增强概述](#一数据增强概述)\n- [二,opencv 图像增强-几何变换](#二opencv-图像增强-几何变换)\n- [三,pytorch 图像增强](#三pytorch-图像增强)\n- [四,imgaug 图像增强](#"
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-数据标准化.md",
"chars": 13370,
"preview": "- [前言](#前言)\n- [一,Normalization 概述](#一normalization-概述)\n - [1.1,Normalization 定义](#11normalization-定义)\n - [1.2,什么情况需要 N"
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-模型可视化.md",
"chars": 590,
"preview": "## 一,模型可视化\n\n模型可视化分为 3 个方向:\n\n- 可视化权重\n- 可视化激活\n- 可视化数据\n- 可视化 feature map\n\n## 参考资料\n\n1. [Visualizing Neural Networks with the"
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-正则化策略.md",
"chars": 876,
"preview": "> 本文内容参考资料为《深度学习》和《解析卷积神经网络》两本书,以及部分网络资料。\n\n## 前言\n\n机器学习中的一个核心问题是设计不仅在训练数据上表现好,并且能在新输入(测试集)上泛化好的算法。\n\n**通过对学习算法的修改,旨在显示地减少泛"
},
{
"path": "4-deep_learning_alchemy/深度学习炼丹-超参数调整.md",
"chars": 13985,
"preview": "- [前言](#前言)\n- [一,网络层内在参数](#一网络层内在参数)\n - [1.1,使用 3x3 卷积](#11使用-3x3-卷积)\n - [1.2,使用 cbr 组合](#12使用-cbr-组合)\n - [1.3,尝试不同的权"
},
{
"path": "5-model_compression/README.md",
"chars": 223,
"preview": "## 一些参考资料\n\n1. [《人工智能系统》](https://github.com/microsoft/AI-System/tree/main/Textbook)\n2. [ncnn](https://github.com/Tencent"
},
{
"path": "5-model_compression/基于pytorch实现模型剪枝.md",
"chars": 9277,
"preview": "- [一,剪枝分类](#一剪枝分类)\n - [1.1,非结构化剪枝](#11非结构化剪枝)\n - [1.2,结构化剪枝](#12结构化剪枝)\n - [1.3,本地与全局修剪](#13本地与全局修剪)\n- [二,PyTorch 的剪枝]"
},
{
"path": "5-model_compression/模型压缩-剪枝算法详解.md",
"chars": 11093,
"preview": "- [一,前言](#一前言)\n - [1.1,模型剪枝定义](#11模型剪枝定义)\n- [二,深度神经网络的稀疏性](#二深度神经网络的稀疏性)\n - [2.1,权重稀疏](#21权重稀疏)\n - [2.2,激活稀疏](#22激活稀疏"
},
{
"path": "5-model_compression/模型压缩-知识蒸馏详解.md",
"chars": 0,
"preview": ""
},
{
"path": "5-model_compression/模型压缩-神经网络量化基础.md",
"chars": 13923,
"preview": "---\nlayout: post\ntitle: 模型压缩-神经网络量化基础\ndate: 2023-03-05 19:00:00\nsummary: 总结线性量化优点、原理、方法和实战基础。\ncategories: Model_Compress"
},
{
"path": "5-model_compression/模型压缩-轻量化网络总结.md",
"chars": 9519,
"preview": "- [前言](#前言)\n- [一些关键字理解](#一些关键字理解)\n - [计算量 FLOPs](#计算量-flops)\n - [内存访问代价 MAC](#内存访问代价-mac)\n - [GPU 内存带宽](#gpu-内存带宽)\n "
},
{
"path": "5-model_compression/深度学习模型压缩方法概述.md",
"chars": 10612,
"preview": "## 一,模型压缩技术概述\n\n我们知道,一定程度上,网络越深,参数越多,模型也会越复杂,但其最终效果也越好,而模型压缩算法是旨在将一个庞大而复杂的大模型转化为一个精简的小模型。\n\n之所以必须做模型压缩,是因为嵌入式设备的**算力和内存有限*"
},
{
"path": "6-model_deploy/AI芯片速览.md",
"chars": 1700,
"preview": "## 一,TPU\n\n张量处理单元 (TPU) 是 Google 设计的**机器学习加速器**,支持 [TensorFlow](https://www.tensorflow.org/?hl=zh-cn)、[Pytorch](https://"
},
{
"path": "6-model_deploy/ONNX模型分析与使用.md",
"chars": 11720,
"preview": "> 本文大部分内容为对 ONNX 官方资料的总结和翻译,部分知识点参考网上质量高的博客。\n\n## 一,ONNX 概述\n\n深度学习算法大多通过计算数据流图来完成神经网络的深度学习过程。 一些框架(例如CNTK,Caffe2,Theano和Te"
},
{
"path": "6-model_deploy/TensorRT基础笔记.md",
"chars": 2002,
"preview": "## 一,概述\n\n`TensorRT` 是 NVIDIA 官方推出的基于 `CUDA` 和 `cudnn` 的高性能深度学习推理加速引擎,能够使深度学习模型在 `GPU` 上进行低延迟、高吞吐量的部署。采用 `C++` 开发,并提供了 `C"
},
{
"path": "6-model_deploy/ncnn源码解析-Net类.md",
"chars": 6831,
"preview": "## 一,网络类 Net\n\n`net.cpp/net.h` 是模型结构定义和模型推理类所在文件,主要包括以下类:\n\n- `Net`: 网络类,主要提供网络结构和网络权重文件加载/解析接口: `load_param` 和 `load_mode"
},
{
"path": "6-model_deploy/ncnn源码解析-sample运行.md",
"chars": 6622,
"preview": "- [一,依赖库知识速学](#一依赖库知识速学)\n - [aarch64](#aarch64)\n - [OpenMP](#openmp)\n - [AVX512](#avx512)\n - [submodule](#submodule)"
},
{
"path": "6-model_deploy/卷积神经网络复杂度分析.md",
"chars": 23814,
"preview": "- [前言](#前言)\n- [一 模型计算量分析](#一-模型计算量分析)\n - [1.1 计算利用率(Utilization)](#11-计算利用率utilization)\n- [二 模型参数量计算](#二-模型参数量计算)\n - ["
},
{
"path": "6-model_deploy/模型压缩部署概述.md",
"chars": 4997,
"preview": "\n- [计算机相关性能指标](#计算机相关性能指标)\n- [一,模型在线部署](#一模型在线部署)\n - [1.1,深度学习项目开发流程](#11深度学习项目开发流程)\n - [1.2,模型训练和推理的不同](#12模型训练和推理的不同"
},
{
"path": "6-model_deploy/模型推理加速技巧-融合卷积和BN层.md",
"chars": 433,
"preview": "## 前言\n\n**模型推理的两个主要挑战是延迟和成本**。\n\n## 参考资料\n\n1. [Speeding up model with fusing batch normalization and convolution](https://l"
},
{
"path": "LICENSE",
"chars": 11336,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 6552,
"preview": "- [项目概述](#项目概述)\n- [我的自制大模型推理框架课程介绍](#我的自制大模型推理框架课程介绍)\n- [一,数学和编程基础专栏](#一数学和编程基础专栏)\n- [二,神经网络基础部件](#二神经网络基础部件)\n- [三,经典卷积神"
},
{
"path": "images/dl/courgette.log",
"chars": 0,
"preview": ""
},
{
"path": "process_image.py",
"chars": 3378,
"preview": "import os\nimport re\nimport time\nfrom deep_translator import GoogleTranslator\n\n# 支持的图片文件扩展名\nIMAGE_EXTENSIONS = ['.png', '"
},
{
"path": "互联网技术大佬独立博客推荐.md",
"chars": 2608,
"preview": "1,[bang's blog](http://blog.cnbang.net/about/)\n\n17 年就是蚂蚁金服 P8 的前端大佬。博客质量基本都很高,看他的文章会让我得到些思考,比如文章中的 “心流”(我的叫法跟他的不一样,但内核一样"
},
{
"path": "手把手教你注册和使用ChatGPT.md",
"chars": 4529,
"preview": "- [一,何为 ChatGPT](#一何为-chatgpt)\n- [二,ChatGPT 注册步骤](#二chatgpt-注册步骤)\n - [2.1,准备条件](#21准备条件)\n - [2.2,注册虚拟电话号码](#22注册虚拟电话号码"
}
]
// ... and 5 more files (download for full content)
About this extraction
This page contains the full source code of the HarleysZhang/dl_note GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 86 files (604.7 KB), approximately 286.5k tokens, and a symbol index with 157 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.