[
  {
    "path": ".gitignore",
    "content": ".DS_Store"
  },
  {
    "path": "1-math_ml_basic/grpc基础笔记.md",
    "content": "## 一，ProtoBuf 基础\n\n在网络通信和通用数据交换等应用场景中经常使用的技术除了 `JSON` 和 `XML`，另外一个就是 `ProtoBuf`。protocol buffers （ProtoBuf）是一种语言无关、平台无关、可扩展的序列化结构数据的方法，它可用于（数据）通信协议、数据存储等。\n\n我们可以通过 ProtoBuf 定义数据结构，然后通过 ProtoBuf 工具生成各种语言版本的数据结构类库，用于操作 ProtoBuf 协议数据。\n\n### 1.1，ProtoBuf 例子\n\n使用 `gRPC` 主要分为三步：\n\n1. 编写 `.proto` `pb` 文件，制定通讯协议。\n2. 利用对应插件将 `.proto` pb 文件编译成对应语言的代码。\n3. 根据生成的代码编写业务代码。\n\n例子，文件名: `response.proto`。通过 ProtoBuf 语法定义数据结构(消息)，这些定义好的数据结构保存在 `.proto` 为后缀的文件中。\n\n```protobuf\n// 指定 protobuf 的版本，proto3 是最新的语法版本\nsyntax = \"proto3\";\n\n// 定义数据结构，message 你可以想象成java的class，c语言中的struct\nmessage Response {\n  string data = 1;   // 定义一个string类型的字段，字段名字为data, 序号为1\n  int32 status = 2;   // 定义一个int32类型的字段，字段名字为status, 序号为2\n}\n```\n\n> 说明：我们通常将 protobuf 消息定义保存在 .proto 为后缀的文件中，字段后面的序号，不能重复，定义了就不能修改，可以理解成字段的唯一 ID。\n\n\n**消息**（`message`），在 `protobuf` 中指的就是我们要定义的数据结构。在上面的例子中，我们定义了一个消息，名字为 `Response`，它有两个字段，一个是 `data`，一个是 `status`。`data` 字段的类型是 `string`，`status`` 字段的类型是 `int32`。\n\n### 1.2，Protobuf 文件编译\n\n从 .proto 文件生成了什么？\n\n当用 protocol buffer 编译器来运行 .proto 文件时，编译器将生成所选择语言的代码，这些代码可以操作在 .proto 文件中定义的消息类型，包括获取、设置字段值，将消息序列化到一个输出流中，以及从一个输入流中解析消息。\n\n对 Python 来说，Python 编译器为 .proto 文件中的每个消息类型生成一个含有**静态描述符**的模块，该模块与一个元类（`metaclass`）在运行时（`runtime`）被用来创建所需的 Python 数据访问类。\n\n\n## 二，Python gRPC 基础\n\n### 2.1，gRPC 定义\n\n`gRPC` 是一种现代化开源的高性能 `RPC` 框架，能够运行于任意环境之中，最初由谷歌进行开发，它使用 `HTTP/2` 作为传输协议。\n\n> RPC（Remote Procedure Call），即远程过程调用，主要是为了解决在分布式系统中，服务之间的调用问题。\n\n`gRPC` 也是基于以下理念：**定义一个服务，指定其能够被远程调用的方法**（包含参数和返回类型），即在 `gRPC` 里，客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法，帮助你更容易创建分布式应用程序和服务。\n- 在服务端，实现这个接口并且运行 `gRPC` 服务器来处理客户端调用。\n- 在客户端，有一个 `stub ` （存根）提供和服务端相同的方法。\n\n![grpc ](../images/python_grpc/grpc.svg)\n\n### 2.2，gRPC 优点\n\n使用 gRPC， 我们**可以一次性的在一个 `.proto` 文件中定义服务并使用任何支持它的语言去实现客户端和服务端**，反过来，它们可以应用在各种场景中，从 Google 的服务器到你自己的平板电脑——gRPC 帮你解决了不同语言及环境间通信的复杂性。使用 protocol buffers 还能获得其他好处，包括高效的序列化，简单的IDL以及容易进行接口更新。\n> `gRPC` 默认使用的 `protocol buffers`，是 Google 开源的一种轻便高效的结构化数据存储格式，可以用于结构化数据串行化，或者说序列化。它很适合做**数据存储**或 **RPC 数据交换**格式。\n\n总结就是，**使用 gRPC 能让我们更容易编写跨语言的分布式代码**。\n\n### 2.3，gRPC 开发步骤\n\n### 2.3.1，编写 .proto 文件定义服务（Defining the service）\n\n像许多 RPC 系统一样，gRPC 基于定义服务的思想，指定可以通过参数和返回类型远程调用的方法。默认情况下，gRPC 使用 protocol buffers 作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。\n\n`proto` 文件主要三要素：**服务、方法、消息**。下面使用 protocol buffers 定义了一个 RouteGuide 服务的例子。\n\n1，服务：\n\n```protobuf\n// 定义服务\nservice RouteGuide {\n   // (Method definitions not shown)\n    ...\n}\n```\n\n2，接下来，在服务定义内部定义 `rpc` **方法**，指定它们的请求和响应类型，所有这些方法都在 `RouteGuide` 服务中使用：\n\n```protobuf\n// 定义方法\n// Accepts a stream of RouteNotes sent while a route is being traversed,\n// while receiving other RouteNotes (e.g. from other users).\nrpc RouteChat(stream RouteNote) returns (stream RouteNote) {}\n```\n\n3，`.proto` 文件还应包含服务方法中使用的所有请求和响应类型的协议缓冲**消息类型**定义，例如 `Point` 消息类型：\n\n```protobuf\n// 定义消息\n// Points are represented as latitude-longitude pairs in the E7 representation\n// (degrees multiplied by 10**7 and rounded to the nearest integer).\n// Latitudes should be in the range +/- 90 degrees and longitude should be in\n// the range +/- 180 degrees (inclusive).\nmessage Point {\n  int32 latitude = 1;\n  int32 longitude = 2;\n}\n```\n\n完整的 `route_guide.proto` 文件如下：\n\n```protobuf\nsyntax = \"proto3\";\n\noption java_multiple_files = true;\noption java_package = \"io.grpc.examples.routeguide\";\noption java_outer_classname = \"RouteGuideProto\";\noption objc_class_prefix = \"RTG\";\n\npackage routeguide;\n\n// Interface exported by the server.\nservice RouteGuide {\n  // A simple RPC.\n  //\n  // Obtains the feature at a given position.\n  //\n  // A feature with an empty name is returned if there's no feature at the given\n  // position.\n  rpc GetFeature(Point) returns (Feature) {}\n\n  // A server-to-client streaming RPC.\n  //\n  // Obtains the Features available within the given Rectangle.  Results are\n  // streamed rather than returned at once (e.g. in a response message with a\n  // repeated field), as the rectangle may cover a large area and contain a\n  // huge number of features.\n  rpc ListFeatures(Rectangle) returns (stream Feature) {}\n\n  // A client-to-server streaming RPC.\n  //\n  // Accepts a stream of Points on a route being traversed, returning a\n  // RouteSummary when traversal is completed.\n  rpc RecordRoute(stream Point) returns (RouteSummary) {}\n\n  // A Bidirectional streaming RPC.\n  //\n  // Accepts a stream of RouteNotes sent while a route is being traversed,\n  // while receiving other RouteNotes (e.g. from other users).\n  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}\n}\n\n// Points are represented as latitude-longitude pairs in the E7 representation\n// (degrees multiplied by 10**7 and rounded to the nearest integer).\n// Latitudes should be in the range +/- 90 degrees and longitude should be in\n// the range +/- 180 degrees (inclusive).\nmessage Point {\n  int32 latitude = 1;\n  int32 longitude = 2;\n}\n\n// A latitude-longitude rectangle, represented as two diagonally opposite\n// points \"lo\" and \"hi\".\nmessage Rectangle {\n  // One corner of the rectangle.\n  Point lo = 1;\n\n  // The other corner of the rectangle.\n  Point hi = 2;\n}\n\n// A feature names something at a given point.\n//\n// If a feature could not be named, the name is empty.\nmessage Feature {\n  // The name of the feature.\n  string name = 1;\n\n  // The point where the feature is detected.\n  Point location = 2;\n}\n\n// A RouteNote is a message sent while at a given point.\nmessage RouteNote {\n  // The location from which the message is sent.\n  Point location = 1;\n\n  // The message to be sent.\n  string message = 2;\n}\n\n// A RouteSummary is received in response to a RecordRoute rpc.\n//\n// It contains the number of individual points received, the number of\n// detected features, and the total distance covered as the cumulative sum of\n// the distance between each point.\nmessage RouteSummary {\n  // The number of points received.\n  int32 point_count = 1;\n\n  // The number of known features passed while traversing the route.\n  int32 feature_count = 2;\n\n  // The distance covered in metres.\n  int32 distance = 3;\n\n  // The duration of the traversal in seconds.\n  int32 elapsed_time = 4;\n}\n```\n\n值得注意的是， 在gRPC 中我们可以定义**四种**类型的服务方法。\n\n1. **普通 rpc**: 客户端向服务器发送一个请求，然后得到一个响应，就像普通的函数调用一样。\n\n```protobuf\nrpc SayHello(HelloRequest) returns (HelloResponse);\n```\n\n2. **服务器流式 rpc**: 其中客户端向服务器发送请求，并获得一个流来读取一系列消息。客户端从返回的流中读取，直到没有更多的消息。gRPC 保证在单个 RPC 调用中的消息是有序的。\n\n```protobuf\nrpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);\n```\n\n3. **客户端流式 rpc**: 其中客户端写入一系列消息并将其发送到服务器，同样使用提供的流。一旦客户端完成了消息的写入，它就等待服务器读取消息并返回响应。同样，gRPC 保证在单个 RPC 调用中对消息进行排序。\n```protobuf\nrpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);\n```\n\n4. **双向流式 rpc**: 其中双方使用读写流发送一系列消息。这两个流独立运行，因此客户端和服务器可以按照自己喜欢的顺序读写: 例如，服务器可以等待接收所有客户端消息后再写响应，或者可以交替读取消息然后写入消息，或者其他读写组合。每个流中的消息是有序的。\n\n### 2.2，生成指定语言的代码-生成客户端和服务器代码（Generating client and server code）\n\n前面那节内容，我们知道了如何在 `proto` 文件中**定义服务**，接下来我们需要使用 `gRPC` 的协议编译器 `protoc` 从 `.proto` 文件中**生成客户端和服务器代码**:\n\n- 在服务器端，**服务器实现服务声明的方法**，并运行一个 gRPC 服务器来处理客户端发来的调用请求。gRPC 底层会对传入的请求进行解码，执行被调用的服务方法，并对服务响应进行编码。\n- 在客户端，客户端有一个称为存根（`stub`）的本地对象，它实现了与服务相同的方法。然后，客户端可以在本地对象上调用这些方法，将调用的参数包装在适当的 protocol buffers 消息类型中——gRPC 在向服务器发送请求并返回服务器的 protocol buffers 响应之后进行处理。\n\n```bash\n# First, install the grpcio-tools package:\n$ pip install grpcio-tools\n# Use the following command to generate the Python code:\n$ python -m grpc_tools.protoc -I../../protos --python_out=. --pyi_out=. --grpc_python_out=. ../../protos/route_guide.proto\n```\n\n**命令说明**：\n- `-I`: proto 协议文件目录\n- `--python_out` 和 `--grpc_python_out` 生成 python 代码的目录\n- 命令最后面的参数是 `proto` 协议文件路径\n\n命令执行后生成 route_guide_pb2.py 文件和 route_guide_pb2_grpc.py 文件。\n- `route_guide_pb2.py`: 主要包含 proto 文件定义的消息类。\n- `route_guide_pb2_grpc.py`: **包含服务端和客户端代码**，比如：\n  - `RouteGuideStub`，客户端可以使用它调用 RouteGuide RPCs\n  - `RouteGuideServicer`，定义 RouteGuide 服务的实现接口\n  - `add_RouteGuideServicer_to_server`: 将 route_guide.proto 中定义的服务的函数 RouteGuideServicer 添加到 grpc.Server\n\n### 2.3，编写业务逻辑代码-创建和实现服务端（Implementing the server）\n\n**gRPC 帮我们解决了 RPC 中的服务调用、数据传输以及消息编解码，我们剩下的工作就是要编写业务逻辑代码**。\n\n而创建和运行 RouteGuide 服务器可分为两个主要步骤：\n\n1. 根据前面由 `proto` 服务定义生成的**服务程序接口**，开始编写实际可运行的服务接口代码，注意是包含执行服务的**实际**“工作”的函数。\n2. 运行一个 `gRPC` 服务器，以侦听客户端的请求并传输响应。\n\n可以在 grpc 的仓库的 [examples/python/route_guide/route_guide_server.py](https://github.com/grpc/grpc/tree/master/examples/python/route_guide) 中找到示例 `RouteGuide` 服务器代码。\n\nroute_guide_server.py 有一个 RouteGuideServicer 类，它是生成的类 route_guide_pb2_grpc.RouteGuideServicer 的子类：\n\n```python\n# RouteGuideServicer provides an implementation of the methods of the RouteGuide service.\nclass RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):\n```\n![RouteGuideServicer](../images/python_grpc/RouteGuideServicer.png)\n\n#### 2.3.1，响应流式 RPC（Response-streaming RPC）\n\n```python\ndef ListFeatures(self, request, context):\n    left = min(request.lo.longitude, request.hi.longitude)\n    right = max(request.lo.longitude, request.hi.longitude)\n    top = max(request.lo.latitude, request.hi.latitude)\n    bottom = min(request.lo.latitude, request.hi.latitude)\n    for feature in self.db:\n        if (\n            feature.location.longitude >= left\n            and feature.location.longitude <= right\n            and feature.location.latitude >= bottom\n            and feature.location.latitude <= top\n        ):\n            yield feature\n```\n\n\"ListFeatures\" 方法是实现 Protocol Buffer 文件中定义的 \"ListFeatures\" 服务器到客户端流式 RPC 的方法。它接受一个类型为 \"Rectangle\" 的请求对象和一个上下文对象作为参数。它计算给定矩形的边界框，并迭代 RouteGuide 数据库中的特征。对于每个落在边界框内的特征，它将特征发送给客户端。\n\n在这里，request.lo.longitude 和 request.hi.longitude 分别表示矩形的左下角和右上角的经度。因为经度越往左越小，所以这行代码使用 min() 函数来计算这两个经度值中的最小值，从而得到矩形的左边界。\n\n#### 2.3.2，双向流式 RPC（Bidirectional streaming RPC）\n\n```python\ndef RouteChat(self, request_iterator, context):\n    prev_notes = []\n    for new_note in request_iterator:\n        for prev_note in prev_notes:\n            if prev_note.location == new_note.location:\n                yield prev_note\n        prev_notes.append(new_note)\n```\n\n`RouteChat` 方法在 Protocol Buffer 文件中被定义为一个双向流式 `RPC`。它接受一个流式的请求对象 `RouteNote ` 和一个上下文对象作为参数，并返回一个流式的响应对象 `RouteNote`。\n\n#### 2.3.3，启动服务器（Starting the server）\n\n实现所有 `RouteGuide` 方法后，下一步是启动 `gRPC` 服务器，以便客户端可以实际使用您的服务：\n\n```python\ndef serve():\n    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))\n    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(\n        RouteGuideServicer(), server\n    )\n    server.add_insecure_port(\"[::]:50051\")\n    server.start()\n    server.wait_for_termination()\n```\n\nserver.start() 方法是非阻塞的。它会创建一个新的线程来处理请求。调用 server.start() 的线程通常在此期间不需要做其他工作。在这种情况下，您可以调用 server.wait_for_termination() 方法，以清晰地阻塞调用线程，直到服务器终止。\n\n### 2.4，创建和实现客户端（Creating the client）\n\n要调用服务方法，我们首先需要创建一个**存根**（stub）。 我们实例化从 ``.proto`` 生成的 `route_guide_pb2_grpc` 模块中的 `RouteGuideStub` 类。\n\n```python\nchannel = grpc.insecure_channel('localhost:50051')\nstub = route_guide_pb2_grpc.RouteGuideStub(channel)\n```\n\n#### 2.4.1，调用服务方法\n\n- 对于返回单个响应的 RPC 方法（“响应-单一”方法），gRPC Python 支持同步（阻塞）和异步（非阻塞）的控制流语义。\n- 对于响应流式传输的 RPC 方法，调用会立即返回一个响应值的迭代器。对该迭代器调用 next() 方法会阻塞，直到从迭代器中产生的响应可用为止。\n\n#### 2.4.2，简单 RPC\n\n对于简单 RPC GetFeature 的同步调用几乎与调用本地方法一样简单。RPC 调用会等待服务器响应，然后将返回响应或引发异常：\n\n```python\nfeature = stub.GetFeature(point)\n```\n\n### 2.4，运行客户端和服务器（Running the client and server）\n\n```bash\n# Run the server:\n$ python route_guide_server.py\n# From a different terminal, run the client:\n$ python route_guide_client.py\n```\n\n## 三，python 协程\n\n我们知道，函数（子程序）调用是通过**栈**实现的，子程序调用总是一个入口紧跟着一次返回，**调用顺序是明确的**。\n\n而协程的调用和子程序不同，协程看上去也是子程序，但执行过程中，**在子程序内部可中断**，然后转而执行别的子程序，在**适当**的时候再返回来接着执行。注意，这里在一个子程序的内部中断，去执行其他子程序，不是函数调用，有点类似 CPU 的中断。\n> 学习过单片机的应该能理解这里的中断的概念。\n\n协程的特点在于是一个线程执行，和多线程比，最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换， 而是由程序自身控制，因此，没有线程切换的开销，**和多线程比，线程数量越多，协程的性能优势就越明显**。\n\n第二大优势就是不需要多线程的锁机制，因为只有一个线程，也不存在同时写变量冲突，在协程中控制共享资源不加锁，只需要判断状态就好了，所以执行效率比多线程高很多。\n\n对于多核心 cpu，可以考虑使用多进程 + 协程的方式，充既充分利用多核，又充分发挥协程的高效率，可获得极高的性能。\n\n### 3.1，python 协程实践\n\n`asyncio` 是 Python 3.4 版本引入的标准库，直接内置了对异步 IO 的支持。\n\nPython 对协程的支持是通过 generator 实现的。`yield from`（python3.5 版本之后是 `await`） 语法可以让我们方便地调用另一个 `generator`。\n\n传统的生产者-消费者模型是一个线程写消息，一个线程取消息，通过 锁机制控制队列和等待，但一不小心就可能死锁。\n\n如果改用协程，生产者生产消息后，直接通过 yield 跳转到消费者开始 执行，待消费者执行完毕后，切换回生产者继续生产，效率极高。\n\n### 3.2，asyncio 库学习\n\n`asyncio.run` 函数是 Python 3.7 版本引入的一个工具函数，用于运行一个异步函数并管理整个异步事件循环的生命周期。使用 asyncio.run 的典型场景是在脚本或应用程序的顶层部分运行一个异步函数，而不必手动管理事件循环的创建和关闭。\n\n```python\nimport asyncio\n\nasync def my_async_function():\n    print(\"Running async function\")\n\n# asyncio.run 负责创建事件循环、运行 my_async_function 函数，并在函数执行完成后关闭事件循环。\nasyncio.run(my_async_function())\n```\n\n### 3.3，grpc.aio.server 和 grpc.server 函数的区别\n\ngrpc.aio.server 和 grpc.server 都是 gRPC Python 库提供的服务器创建函数，但它们在处理异步请求和同步请求方面有所不同。\n\n1. `grpc.aio.server`：\n  - grpc.aio.server 是基于 asyncio（异步 I/O）的 gRPC 服务器实现。\n  - 支持异步请求处理，适用于异步 Python 代码。\n  - 允许使用 async def 声明的异步处理函数。\n  - 通常与 asyncio.run 一起使用。\n\n2. `grpc.server`：\n  - grpc.server 是传统的 gRPC 服务器实现，采用同步请求处理方式。\n  - 使用标准的 gRPC 处理函数，不支持异步处理。\n  - 适用于传统的同步 Python 代码\n\n## 四，Typer 库基础\n\n`Typer` 是一个用于构建 `CLI` 应用程序的库，它简化了 CLI 应用程序的创建和使用过程。它是基于 `Click` 库构建的，提供了更简单的 API。\n> CLI 是 Command Line Interface 的缩写，中文翻译为命令行界面。它是一种通过纯文本命令进行交互的用户界面，用户通过键入命令来执行特定的操作。\n\n安装方法：\n```\npip install \"typer[all]\"\n```\n\n### 4.1，包含两个子命令的简单示例\n\n以下是使用 `typer.Typer()` 创建一个带有**两个子命令**及其参数的 `CLI` 应用的基本示例：\n\n```python\nimport typer\n\napp = typer.Typer()\n\n@app.command()\ndef hello(name: str):\n    print(f\"Hello {name}\")\n\n@app.command()\ndef goodbye(name: str, formal: bool = False):\n    if formal:\n        print(f\"Goodbye Ms. {name}. Have a good day.\")\n    else:\n        print(f\"Bye {name}!\")\n\nif __name__ == \"__main__\":\n    app()\n```\n\n程序运行结果：\n\n![Typer](../images/python_grpc/app_example.png)\n\n## 参考资料\n\n1. [ProtoBuf 快速入门教程](https://www.tizi365.com/archives/367.html)\n2. [grpc-Basics tutorial](https://grpc.io/docs/languages/python/basics/#client)\n3. [gRPC教程](https://www.liwenzhou.com/posts/Go/gRPC/)"
  },
  {
    "path": "1-math_ml_basic/nlp背景知识总结.md",
    "content": "- [词向量](#词向量)\n  - [`One-Hot` 编码](#one-hot-编码)\n  - [Word Embedding](#word-embedding)\n- [vocab 和 merge table](#vocab-和-merge-table)\n- [Token ID 序列](#token-id-序列)\n- [embedding 维度](#embedding-维度)\n- [Word Embedding Vector](#word-embedding-vector)\n- [nn.Linear](#nnlinear)\n- [参考资料](#参考资料)\n\n### 词向量\n\n在 CV 领域，需要将数字图像转换为**矩阵/张量**进行神经网络计算；而在 NLP 领域，自然语言通常以文本形式存在，同样需要将文本数据转换为一系列的**数值**方便计算机进行计算，这里会涉及到**词向量**的概念，转换的方法通常有:\n\n- `One-Hot` 编码: 一种简单的单词表示方式\n- `Word Embedding`: 一种分布式单词表示方式\n- `Word2Vec`: 一种词向量的训练方法\n\n#### `One-Hot` 编码\n\n`One-hot` 编码是一种很简单的将单词数值化的方式。对于单词数量为 N 的词表，则需用一个长度为 N 的向量表示一个单词，在这个向量中该单词对应的位置数值为1，其余单词对应的位置数值全部为0。举例如下：\n\n**词典**: [queen, king, man, woman, boy, girl ]\n\n![one-hot 编码图](../../images/llm_basic/one-hot-eg.png)\n\n上图展示了词典中 `6` 个单词的 one-hot 编码表示。虽然这个表示还是很简单的，但是其也存在以下问题:\n\n- 现实当中单词数量往往有几十万甚至上百万，这样如果用 one-hot 编码的方式表示单词，其向量维度会很长，且极其稀疏，即**高维高稀疏**。\n- 因为向量之间是正交且点积为 0，所以无法直接通过向量计算的方式来得出单词之间的关系，即**无法看出词之间的语义相似性**。\n\n#### Word Embedding\n\nWord Embedding 的概念。如果将 word 看作文本的最小单元，可以将 Word Embedding 理解为一种**映射**，其过程是：将文本空间中的某个 word，通过一定的方法，映射或者说嵌入（embedding）到另一个数值向量空间（之所以称之为 embedding，是因为这种表示方法往往伴随着一种**降维**的意思）\n\n1，基于频率的 Word Embedding 又可细分为如下几种：\n\n- Count Vector\n- TF-IDF Vector\n\n### vocab 和 merge table\n\n在自然语言处理中，`Token` 是指一段文本中的基本单位，通常是一个词、一个词组或者一个字符。Tokenization 是将一段文本分解为一系列的 Token 的过程。\n\n`vocab` 文件和 `merge table` 可以用来将原始文本分割成一系列的 `token`。\n\n1，Vocab 文件，全称为 vocabulary file，是指包含了所有可能出现在文本中的 `token` 列表。在 LLM 中，每个 token 对应着一个编号（或者叫做词汇表中的索引），以便在模型中表示为对应的向量。Vocab 文件中的每个token 一般都是由一个或多个字符组成的，通常会包括汉字、英文单词、标点符号等。以下是一个示例vocab文件的部分内容：\n\n```bash\ncsharpCopy code\n[PAD]\n[UNK]\n[CLS]\n[SEP]\n[MASK]\n我\n的\n你\n是\n了\n...\n```\n\n在这个例子中，前五个 token 是特殊 token，分别表示填充、未知、开始、结束和掩码，其余的token是由汉字和英文单词组成的。在 LLM 中，所有输入文本的 token 都会映射为这个 vocab 文件中的其中一个 token。\n\n2，`Merge Table` 是 LLM 中另一个重要的概念，它用于将文本中的字符逐步合并为**更大的 token**。Merge Table包含了一系列的合并操作，每个操作都是由两个字符组成的，表示将这两个字符合并为一个新的 token。\n\n以下是一个示例的 Merge Table 的部分内容：\n\n```bash\nL U\nK S\ne r\no v\no l\noo o\n```\n\n在这个例子中，每个操作将两个字符合并为一个新的 token，例如将 L 和 U 合并为一个新的 token LU，将 e 和 r合并为一个 token `er` 等等。这些操作的顺序和次数都是由 LLM 模型自动学习得到的，通过这些合并操作，LLM可以将任意长度的文本逐步分割成一系列的 token，然后再进行模型预测。\n\n### Token ID 序列\n\n在自然语言处理中，计算机无法直接处理文本，需要将文本转换为数字形式进行处理。`Token ID` 序列就是这样一种**数字形式**，它可以被输入到神经网络或其他机器学习算法中进行训练和预测。\n\n具体来说，`Token ID` 序列是指将原始文本序列中的每个单词或子词（`token`）映射为对应的 ID 后得到的**整数序列**。在使用 `tokenizer` 对文本进行编码时，tokenizer 会将每个单词或子词映射为一个唯一的 ID，得到 token ID 序列作为模型的输入。\n\n### embedding 维度\n\n在自然语言处理任务中，会将输入的文本序列通过 `embedding` 层**转换为固定维度的向量序列**，然后再通过后续的神经网络层进行处理。embedding 层的输出维度称为 embedding 维度（`embed_dim`），是一个超参数，需要根据具体任务和数据集进行调整。\n\n### Word Embedding Vector\n\n`Word Embedding Vector` 是将单词映射到向量空间的一种方式，它可以**将单词的语义信息转化为向量形式**，并且可以在向量空间中计算单词之间的相似性，简单理解就是，将单词从原始文本格式转换为神经网络可以理解和处理的数字格式的一种方式。在深度学习中，我们通常使用 Word2Vec、GloVe 或 FastText 等算法来生成单词的嵌入向量。\n\n下面是一个简单的 Python 示例，用于使用 PyTorch 生成单词的嵌入向量：\n\n```python\nimport torch\nimport torch.nn as nn\nfrom torchtext.vocab import Vocab\n\nfrom collections import Counter\n\ntext = \"hello world hello\" # 假设我们有一个文本\ncounter = Counter(text.split()) # 统计每个单词的出现次数\nvocab = Vocab(counter) # 创建词汇表\n\n# 构建嵌入层\nvocab_size = 1000\nembedding_dim = 100\nembedding_layer = nn.Embedding(vocab_size, embedding_dim)\n\n# 假设我们有一个输入单词列表，每个单词都是从词汇表中随机选择的\ninput_words = [\"hello\", \"world\", \"this\", \"is\", \"a\", \"test\"]\n\n# 将单词转换为索引列表\nword_indexes = [vocab.index(word) for word in input_words]\n\n# 将索引列表转换为PyTorch张量\nword_indexes_tensor = torch.LongTensor(word_indexes)\n\n# 将索引列表输入嵌入层以获取嵌入向量\nword_embeddings = embedding_layer(word_indexes_tensor)\n\n# 输出嵌入向量\nprint(word_embeddings)\n```\n\n1.  首先通过 `Vocab` 和输入文本字符串创建词汇表 `vocab`，并通过 `nn.Embedding` 创建了一个大小为 $1000\\times 100$ 的嵌入层 `embedding_layer`，表示**词汇表**中有 `1000` 个单词，每个单词用一个 `100` 维的向量表示。\n2. 然后，我们创建一个输入单词列表 input_words，并将每个单词转换为词汇表中对应的索引，和将索引列表转换为 PyTorch 张量。\n3. 最后将张量 `word_indexes_tensor` 输入到嵌入层中，以获取每个单词的 100 维嵌入向量。\n\n值得注意的是，嵌入向量的大小和维度通常需要根据任务进行调整。通常，词汇表越大，嵌入向量的维度就越高。此外，**嵌入向量的大小通常需要与模型中的其他参数相匹配，例如隐藏层的大小和注意力头的数量**。\n\n### nn.Linear\n\n在 PyTorch 中，`nn.Linear` 是一个模块，它实现了一个全连接层，它将输入张量的每个元素都乘以一个可学习的权重矩阵，并加上一个可学习的偏置向量，最后输出一个新的张量。全连接层的数学表达式为：\n$$\n\\text{output} = \\text{input} \\times \\text{weight}^\\text{T} + \\text{bias} \\nonumber\n$$\n`nn.Linear` 的构造函数如下：\n\n```python\nnn.Linear(in_features: int, out_features: int, bias: bool = True)\n```\n\n各参数解释如下：\n\n- `in_features` 表示输入张量的特征数，也就是输入张量的最后一维的大小，\n- `out_features` 表示输出张量的特征数，也就是输出张量的最后一维的大小，\n- `bias` 是一个布尔值，表示是否添加偏置向量。如果 `bias` 为 `False`，则不会添加偏置向量。\n\n下面是一个示例代码，可直接运行，其创建了一个输入张量，并通过 层进行维度的线性变换。\n\n```python\nimport torch.nn as nn\nimport torch\nimport time\n\n# 创建一个输入张量\ninput_tensor = torch.randn(10, 20)\n\n# 创建一个 Linear 层对象，将输入维度从 20 变换到 30\nlinear_layer = nn.Linear(20, 30)\n\n# 将输入张量传递给 Linear 层进行变换\nstart_time = time.time()\noutput_tensor = linear_layer(input_tensor)\nend_time = time.time()\n\n# 打印输出张量的形状\nprint(f\"Output shape: {output_tensor.shape}\") # Output shape: torch.Size([10, 30])\nprint(f\"Time taken: {end_time - start_time:.6f} seconds\") # Time taken: 0.002543 seconds\n```\n\n> 深度学习论文公式和代码可视化见 github 仓库 [annotated_deep_learning_paper_implementations](https://github.com/labmlai/annotated_deep_learning_paper_implementations)。\n\n### 参考资料\n\n- https://paddlepedia.readthedocs.io/en/latest/tutorials/sequence_model/word_representation/index.html\n- [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)"
  },
  {
    "path": "1-math_ml_basic/rust编程基础.md",
    "content": "## 认识 Cargo\n\n`cargo` 是 `Rust` 的包管理工具，提供了从项目的建立、构建到测试、运行乃至部署的完整功能，与 `Rust` 语言及其编译器 `rustc` 紧密结合。\n\n1，创建项目。\n\n```bash\n$ cargo new world_hello\n$ cd world_hello\n```\n\n创建的项目结构：\n\n```console\n$ tree\n.\n├── .git\n├── .gitignore\n├── Cargo.toml\n└── src\n    └── main.rs\n```\n\n2，两种方式可以运行项目：\n\n- `cargo run release`（默认是 debug），相当于执行了两个命令 cargo build 编译项目，和 ./target/debug/world_hello。\n- 手动编译和运行项目。\n\n3，`Cargo.toml` 和 `Cargo.lock`\n\n-  `Cargo.toml` 是一种轻量级的配置文件格式，用于配置项目的元信息、依赖关系、构建选项等。\n- `Cargo.lock` 文件是 `cargo` 工具根据同一项目的 `toml` 文件生成的项目依赖详细清单，因此我们一般不用修改它。\n\n## 一，宏\n\n1， `Package` 是一个项目工程，而包只是一个编译单元，`src/main.rs` 和 `src/lib.rs` 都是编译单元，因此它们都是包。\n\n2， 一个 `crate` 可以是二进制（`src/main.rs`）或者库（`src/lib.rs`），每一个包（crate）都有包跟（ crate root），例如二进制包的包根是 `src/main.rs`，库包的包根是 `src/lib.rs`。它是编译器开始处理源代码文件的地方，同时也是包模块树的根部。\n\n3，`rust` 出于安全考虑，默认情况下，所有的类型都是私有化的，包括函数、方法、结构体、枚举、常量，是的，就连模块本身也是私有化的。在 Rust 中，**父模块完全无法访问子模块中的私有项**，反之，子模块可以访问父模块。\n\n4，模块可见性不代表模块内部项的可见性，模块的可见性仅仅是允许其它模块去引用它，但是想要引用它内部的项，还得继续将对应的项标记为 `pub`。\n\n5，使用 `super` 引用模块，`super` 代表的是父模块为开始的引用方式。\n\n```rust\nfn serve_order() {}\n\n// 厨房模块\nmod back_of_house {\n    fn fix_incorrect_order() {\n        cook_order();\n        super::serve_order(); // 调用了父模块(包根)中的 serve_order 函数\n    }\n\n    fn cook_order() {}\n}\n```\n\n### 1.1，属性\n\n可以使用称被为宏的自定义句法形式来扩展 Rust 的功能和句法。宏需要被命名，并通过一致的句法去调用：`some_extension!(...)`。\n\n定义新宏有两种方式：\n\n- [声明宏(Macros by Example)](https://rustwiki.org/zh-CN/reference/macros-by-example.html)以更高级别的声明性的方式定义了一套新句法规则。\n- [过程宏(Procedural Macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html)可用于实现自定义派生。\n\n#### 1.1.1，过程宏\n\n*过程宏*允许在执行函数时创建句法扩展。过程宏有三种形式:\n\n- [类函数宏(function-like macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#function-like-procedural-macros) - `custom!(...)`\n- [派生宏(derive macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#derive-macros)- `#[derive(CustomDerive)]`\n- [属性宏(attribute macros)](https://rustwiki.org/zh-CN/reference/procedural-macros.html#attribute-macros) - `#[CustomAttribute]`\n\n1，**派生宏**\n派生宏为派生(derive)属性定义新输入。这类宏在给定输入结构体(struct)、枚举(enum)或联合体(union) token流的情况下创建新程序项。它们也可以定义派生宏辅助属性。\n\n2，**属性宏**\n属性宏定义可以附加到程序项上的新的外部属性，这些程序项包括外部(extern)块、固有实现、trate实现，以及 trait声明中的各类程序项。\n\n### 1.2，使用 tracing 记录日志\n\n1，**在于日志只能针对某个时间点进行记录，缺乏上下文信息，而线程间的执行顺序又是不确定的，因此日志就有些无能为力**。而 `tracing` 为了解决这个问题，引入了 `span` 的概念( 这个概念也来自于分布式追踪 )，一个 `span` 代表了一个时间段，拥有开始和结束时间，在此期间的所有类型数据、结构化数据、文本数据都可以记录其中。\n\n2，tracing` 中最重要的三个概念是 `  `Span` 、` Event ` 和 `Collector`。\n\n#### 1.2.1，使用方法-span! 宏\n\n`span!` 宏可以用于创建一个 `Span` 结构体，然后通过调用结构体的 `enter` 方法来开始，再通过超出作用域时的 `drop` 来结束。\n\n#### 1.2.2，使用方法-#[instrument]\n\n如果想要将某个函数的整个函数体都设置为 `span` 的范围，最简单的方法就是为函数标记上 `#[instrument]`，此时 tracing 会自动为函数创建一个 span，span 名跟函数名相同，在输出的信息中还会自动带上函数参数。\n\n## 二，模式匹配\n\nmatch分支匹配的用法非常灵活，它的基本语法为：\n\n```rust\nmatch VALUE {\n  PATTERN1 => EXPRESSION1,\n  PATTERN2 => EXPRESSION2,\n  PATTERN3 => EXPRESSION3,\n}\n```\n\n`match` 匹配的通用形式：\n\n```rust\nmatch target {\n    模式1 => 表达式1,\n    模式2 => {\n        语句1;\n        语句2;\n        表达式2\n    },\n    _ => 表达式3\n}\n```\n\n**match** 支持两种匹配模式（不可反驳的模式(irrefutable) 和可反驳的的模式(refutable)）：\n\n- 当明确给出分支的Pattern时，必须是可反驳模式，这些模式允许匹配失败\n- 使用`_`作为最后一个分支时，是不可反驳模式，它一定会匹配成功\n- 如果只有一个Pattern分支，则可以是不可反驳模式，也可以是可反驳模式\n\n### 2.1，全模式列表\n\n1，匹配字面值\n\n2，匹配命名变量\n\n3，单分支多模式\n\n4，通过序列 ..= 匹配值的范围\n\n5，解构并分解值： 使用模式来解构结构体、枚举、元组、数组和引用。\n\n模式匹配一样要类型相同。\n\n## 三，复合类型\n\n### 3.1，结构体\n\n结构体和元组类似，都是由多种类型组合而成。示例：\n\n```rust\nstruct User {\n    active: bool,\n    username: String,\n    email: String,\n    sign_in_count: u64,\n}\n```\n\n该结构体名称是 `User`，拥有 4 个字段，且每个字段都有对应的字段名及类型声明，例如 `username` 代表了用户名，是一个可变的 `String` 类型。\n\n## 四，难点语法速记\n\n1，Option 代表可能为空可能有值的一种类型，本质上是一个枚举，有两种分别是 Some 和 None。Some 代表有值，None 则类似于 null，代表无值。\n\n2，Result 直接翻译过来就是“结果”，想象一下，我们的接口，服务有非常常规的调用场景，正常返回值，异常返回错误或抛异常等等。而 Rust 里就定义有了一个 Result 用于此场景。Result 内部本质又是一个枚举，内部分别是 Ok 和 Err，是 Ok 时则代表正常返回值，Err 则代表异常。\n\n3，使用 ? 后，你不需要挨个判断并返回，任何一个 ? 返回 Err 了函数都会直接返回 Err。\n\n4，unwrap 和 Option 的一样，正常则拿值，异常则 panic!\n\n## 参考资料\n\n- [rust入门秘籍-模式匹配的基本使用](https://rust-book.junmajinlong.com/ch10/01_pattern_match_basis.html)\n- https://blog.vgot.net/archives/rust-some.html"
  },
  {
    "path": "1-math_ml_basic/src/cpp/binary_search.cpp",
    "content": "#include <stdio.h>\n#include <iostream>\n#include <vector>\n\nusing namespace std;\n\nint binary_search(std::vector<int> arr, int target) {\n    int left = 0;\n    int right = arr.size() - 1;\n    while (left <= right) {\n        auto mid = (left + right) / 2;\n\n        if (arr[mid] == target)\n            return mid;\n        else if (arr[mid] < target) \n            right = mid - 1;\n        else\n            left = left + 1;\n    }\n    return -1;\n}\n\nint main() {\n    std::vector<int> arr({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});\n    int target = 5;\n    auto index = binary_search(arr, target);\n    std::cout << \"index: \" << index << std::endl;\n}"
  },
  {
    "path": "1-math_ml_basic/src/cpp/class_copy_move.cpp",
    "content": "#include <iostream>\n\nclass Complex\n{\npublic:\n    Complex(double r = 0, double i = 0) : real(r), imag(i) {}\n\n    // 重载 + 运算符，用于把两个 Complex 对象相加\n    // 返回类型 函数名(参数列表) const;\n    // 用于声明这个函数是一个常量成员函数，不能修改成员变量\n    Complex operator+(const Complex& other) const\n    {\n        return Complex(real + other.real, imag + other.imag);\n    }\n\n    // 重载 - 运算符，用于把两个 Complex 对象相减\n    Complex operator-(const Complex& other) const\n    {\n        return Complex(real - other.real, imag - other.imag);\n    }\n    \n    // 定义为友元函数，并重载 << 运算符\n    friend std::ostream& operator<<(std::ostream& os, const Complex& c)\n    {\n        os << \"(\" << c.real << \"+\" << c.imag << \"i)\";\n        return os;\n    }\nprivate:\n    double real, imag;\n};\n\nint main()\n{\n    Complex c1(1, 2), c2(3, 4);\n    Complex c3 = c1 + c2;\n    Complex c4 = c1 - c2;\n    std::cout << \"c1 is \" << c1 << std::endl;\n    std::cout << \"c2 is \" << c2 << std::endl;\n    std::cout << \"c1 + c2 result is \" << c3 << std::endl;\n    return 0;\n}\n"
  },
  {
    "path": "1-math_ml_basic/src/cpp/cpp_basic.cpp",
    "content": "#include <iostream>\n#include <vector>\n#include <string>\n\nclass MyClass {\npublic:\n    MyClass() { std::cout << \"Default constructor\" << std::endl; }\n    MyClass(const MyClass& other) { std::cout << \"Copy constructor\" << std::endl; }\n    MyClass(MyClass&& other) { std::cout << \"Move constructor\" << std::endl; }\n\n};\n\nint main() {\n    std::vector<MyClass> v1;\n\n    // 添加对象\n    v1.push_back(MyClass());\n    v1.push_back(MyClass());\n\n    std::cout << \"Before move: \" << std::endl;\n\n    // 打印容器中的对象数量\n    std::cout << \"Size: \" << v1.size() << std::endl;\n\n    // 移动容器中的对象\n    std::vector<MyClass> v2(std::move(v1));\n\n    std::cout << \"After move: \" << std::endl;\n\n    // 打印容器中的对象数量\n    std::cout << \"v1 size: \" << v1.size() << std::endl; // v1已被移动，不再包含任何元素\n    std::cout << \"v2 size: \" << v2.size() << std::endl; // v2包含了v1中的元素\n\n    return 0;\n}\n"
  },
  {
    "path": "1-math_ml_basic/src/cpp/fileWrapper.cpp",
    "content": "#include <stdio.h>\n#include <iostream>\n#include <fstream>\n#include <string>\n\nclass fileWrapper {\npublic:\n    // 构造函数 打开文件\n    explicit fileWrapper(const std::string& filename) : \n    m_file(filename, std::ios::in | std::ios::out | std::ios::app) {\n        if (!m_file.is_open()) {\n            throw std::runtime_error(\"Failed to open file: \" + filename);\n        }\n    }\n\n    // 析构函数 关闭文件\n    ~fileWrapper() {\n        if (m_file.is_open()) {\n            throw std::runtime_error(\"File is need to close\");\n            m_file.close();\n        }\n    }\n\n    void writeLine(const std::string& line) {\n        if (!m_file.is_open()) {\n            throw std::runtime_error(\"Failed to write file, file is close.\");\n        }\n        m_file << line << std::endl;\n    }\n\nprivate:\n    std::fstream m_file;\n};\n\ntemplate <typename T>\nvoid wrapper(T&& val) {\n    // 完美转发\n    std::cout << \"input val is \" << std::forward<T>(val) << std::endl;\n}\n\nint main() {\n    \n    int x = 11;\n    wrapper(67);\n    wrapper(\"hello world\");\n    wrapper(x);\n\n    try {\n        fileWrapper fw(\"test.txt\");\n        fw.writeLine(\"Hello World\");\n    } \n    // 离开作用域自动调用 ~FileWrapper()，关闭文件\n    catch(const std::exception& e) {\n        std::cerr << \"Error: \" << e.what() << std::endl;\n    }\n\n    return 0;\n}\n\n"
  },
  {
    "path": "1-math_ml_basic/src/cpp/multi_thread_demo.cpp",
    "content": "#include <stdio.h>\n#include <iostream>\n#include <thread>\n#include <mutex>\n\nusing namespace std;\n\n// 线程函数\nvoid worker() {\n    std::cout << \"Hello from worker thread \" << std::endl;\n}\n\n// 线程函数对象，但是声明为只调用一次\nvoid simple_do_once(std::once_flag* flag)\n{\n    std::call_once(*flag, [](){ std::cout << \"Simple example: called once\\n\"; });\n}\n\nint main() {\n    std::once_flag flag1;\n    std::thread worker_thread(worker);\n    worker_thread.join();\n\n    std::thread thread_once(simple_do_once, &flag1);\n    if (thread_once.joinable())\n        thread_once.join();\n    return 0;\n}"
  },
  {
    "path": "1-math_ml_basic/src/cpp/test_template.cpp",
    "content": "#include <iostream>\n#include <vector>\n#include <stdexcept>\n#include <string>\n#include <cassert>\n\nusing namespace std;\n\n// 通过枚举类定义设备/后端类型\nenum class Device {\n    CPU,\n    GPU,\n    NPU\n};\n\n// 1. 先声明一个通用的模板类 Tensor，形参包括 “后端（Device）” 和 “数据类型（typename T）”\ntemplate<Device Dev, typename T>\nclass Tensor;\n\n// 2. 通过偏特化：Tensor<CPU, T>，实现 CPU 后端的 Tensor 类设计\n/*特例化哪个参数，哪个参数就不用定义在 template<> 中*/\ntemplate<typename T>\nclass Tensor<Device::CPU, T> {\npublic:\n    // 显式构造函数: 传入 vector 类型张量形状参数\n    explicit Tensor(const vector<size_t> shape): shape(shape) {\n        size = 1;\n        for (auto dim_size:shape) {\n            size *= dim_size;\n        }\n        data.resize(size);\n    }\n    \n    // 简单的张量索引访问接口\n    const T& operator[](size_t index) const {\n        return data[index];\n    }\n    // const 表示这是一个常量成员函数\n    size_t size() const noexcept {\n        return size;\n    }\n    const std::vector<size_t>& shape() const { return shape; }\n\n    // 张量的一些成员函数\n    void add(const Tensor<Device::CPU, T> input_t) {\n        assert size == input_t.size();\n        for (int i=0; i < size; i++) {\n            data[i] += input_t.data[i];\n        }\n    }\n    \n    // Debug: 打印部分数据\n    void printDebug(const std::string& name) const {\n        std::cout << \"[CPU Tensor<\" << typeid(T).name() << \">] \" \n                  << name << \" shape: [\";\n        for (auto s : shape) std::cout << s << \" \";\n        std::cout << \"], data[0] = \" << data[0] << \" ...\\n\";\n    }\n\nprivate:\n    size_t size;\n    std::vector<size_t> shape;\n    std::vector<size_t> data;\n};\n\ntemplate<typename T1,typename T2>\nauto func(const T1& x, const T2& y) {\n    return x + y;\n}\n\n// 模板全特化\ntemplate<>\nauto func(const int& x, const double& y) {\n    return x-y;\n}\n\nint main() {\n    auto result1 = add(3, 5);\n    auto result2 = add(8.7, 9.0);\n    std::cout << \"result1: \" << result1 << std::endl;\n    std::cout << \"result2: \" << result2 << std::endl;\n}\n"
  },
  {
    "path": "1-math_ml_basic/src/python/chatgpt_api_demo.py",
    "content": "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\nmodel_engine = \"text-davinci-003\"\nprompt = \"如何使用openai 官网的 chatgpt 服务，给出详细步骤\"\n\n# Generate a response\ncompletion = openai.Completion.create(\n    engine=model_engine,\n    prompt=prompt,\n    max_tokens=1024,\n    n=1,\n    stop=None,\n    temperature=0.5,\n)\n\n# Get the response text\nmessage = completion.choices[0].text\n\nprint(message)"
  },
  {
    "path": "1-math_ml_basic/src/python/classifynet_torch_to_onnx.py",
    "content": "# -*- coding  : utf-8 -*-\r\n# author      : honggao.zhang\r\n# Create      : 2021-2-20\r\n# Update      : 2021-3-12\r\n# Version     : 0.1.0\r\n# Description : 1, Classify net pytorch convert to onnx model template program.\r\n#               2, Support alxnet、inceptionv3、resnet、shufflenetv2 and so on.\r\n\r\nimport sys, os,argparse\r\nimport os.path as osp\r\ncurrent_dir = osp.abspath(os.path.dirname(__file__))\r\nimport numpy as np\r\n\r\nfrom torchsummary import summary\r\nfrom thop import profile\r\n\r\nimport torch\r\nfrom torch.autograd import Variable\r\n\r\n# Gets the GPU if there is one, otherwise the cpu\r\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\");print(device)\r\ninput_shape = [1, 3, 224, 224]\r\nmodel_file_name = 'ResNet50_with_mask_sparsity_50.pth'  # 模型文件\r\nbase_name = osp.splitext(model_file_name)[0] \r\ncurrent_dir = os.path.abspath(os.path.dirname(__file__))\r\nweight_path = osp.join(current_dir, '../weights/', model_file_name); print(weight_path)\r\n\r\n####################################################################################################################\r\ndef arg_parse():\r\n    parser = argparse.ArgumentParser()\r\n    parser.add_argument('--model', '-m', default = 'shufflenetv2_x1.0')\r\n    parser.add_argument('--weight', '-w', default = None)\r\n    parser.add_argument('--gpu', '-gpu', action = 'store_false')\r\n    args = parser.parse_args()\r\n    return args\r\n\r\n###################################################################################################\r\ndef model_inference(net, input_data, weight_path):\r\n    \"\"\"Load model weight file and inference model.\"\"\"\r\n    # net = torch.load(model_path).to(device)\r\n    if weight_path is not None:\r\n        weight_path_name = osp.splitext(weight_path)[0]\r\n        state_dict = torch.load(weight_path, map_location=lambda storage, loc: storage).to(device)\r\n        # net.load_state_dict(state_dict, strict = False)\r\n        print(\"Load model weight file %s success!\" % weight_path_name)\r\n    else:\r\n        print(\"There is no model weights file!\")\r\n    net.eval()\r\n    with torch.no_grad():     \r\n        output = net(input_data)\r\n        \r\n        print(\"Model output type and shape is \", type(output), output.shape)  # torch.Size([1, 13, 48, 80])\r\n        assert list(output.shape) == [1, 1000]\r\n    return net\r\n\r\ndef torch_to_onnx(torch_model, input_data, onnx_file_path):\r\n    torch.onnx.export(torch_model,\r\n                    input_data,\r\n                    onnx_file_path,\r\n                    verbose=False,\r\n                    opset_version=9,\r\n                    do_constant_folding=True,\t# 是否执行常量折叠优化\r\n                    input_names=[\"input\"],\t    # 输入名\r\n                    output_names=[\"output\"],\t# 输出名\r\n                    )\r\n####################################################################################################################\r\ndef define_net():\r\n    pass\r\n\r\n####################################################################################################################\r\ndef main(net_torch, net_name, input_shape):\r\n    # 1, Define model structure\r\n    net = net_torch\r\n    # 2, Define model input\r\n    input_data = Variable(torch.ones(input_shape)).to(device)\r\n    # 3, Start model inference\r\n    net = model_inference(net, input_data, None)\r\n    # 4, Convert torch model to onnx model\r\n    onnx_file_path = osp.abspath(osp.join(current_dir, '.../data/onnx_model/classifynet', net_name+\".onnx\"))  # onnx 模型权重文件路径定义\r\n    torch_to_onnx(net, input_data, onnx_file_path)\r\n\r\n    # 5, Analysis model FLOPs\r\n    print(tuple(input_shape[1:]))\r\n    summary(net, tuple(input_shape[1:]))\r\n    print(input_data.shape)\r\n    macs, params = profile(net, inputs=(input_data, ))\r\n    print(\"Sum of model ops andparams is\", (macs, params))\r\n    print(\"Convert and analysis model success!\")\r\n \r\n###################################################################################################      \r\nif __name__ == '__main__':  \r\n    args = arg_parse()\r\n    if args.model == 'alexnet':       # Alexnet example\r\n        net_name = 'alexnet'\r\n        from torchvision.models.alexnet import alexnet\r\n        net_torch = alexnet(True).eval()\r\n    elif args.model == 'resnet18':    # ResNet example\r\n        net_name = 'resnet18'\r\n        from torchvision.models.resnet import resnet18\r\n        net_torch = resnet18(True).eval()\r\n    elif args.model=='inception_v3':  # Inception_v3 example\r\n        net_name = 'inception_v3'\r\n        from torchvision.models.inception import inception_v3\r\n        net_torch = inception_v3(True, transform_input=False).eval()\r\n    elif args.model == 'vgg16':       # VGG19 example\r\n        net_name = 'vgg16'\r\n        from torchvision.models.vgg import vgg16\r\n        net_torch = vgg16(True).eval()\r\n    elif args.model == 'densenet121':\r\n        net_name = 'densenet121'\r\n        from torchvision.models.densenet import *\r\n        net_torch = densenet121(True).eval()\r\n    elif args.model == 'MobileNetV2':\r\n        net_name = 'MobileNetV2'\r\n        from torchvision.models.mobilenet import mobilenet_v2\r\n        net_torch = mobilenet_v2(True).eval()\r\n    elif args.model == 'shufflenetv2_x1.0':\r\n        net_name = 'shufflenetv2_x1.0'\r\n        from torchvision.models.shufflenetv2 import shufflenet_v2_x1_0\r\n        net_torch = shufflenet_v2_x1_0(False).eval()\r\n    else:\r\n        raise NotImplementedError()   \r\n    \r\n    if args.gpu:\r\n        net_torch.to(device)\r\n    \r\n    main(net_torch, net_name, input_shape)"
  },
  {
    "path": "1-math_ml_basic/src/python/conv_layer.py",
    "content": "# -*- coding  : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n\nimport torch\nimport time\n\nimport numpy as np\nimport time\n\nclass Conv2D:\n    def __init__(self, input_channels, output_channels, kernel_size):\n        self.input_channels = input_channels\n        self.output_channels = output_channels\n        self.kernel_size = kernel_size\n        self.weights = np.random.randn(output_channels, input_channels, kernel_size, kernel_size)\n        self.bias = np.zeros((output_channels, 1))\n\n    def forward(self, input):\n        batch_size, input_channels, height, width = input.shape\n        padded_input = np.pad(input, ((0, 0), (0, 0), (self.kernel_size // 2, self.kernel_size // 2),\n                                      (self.kernel_size // 2, self.kernel_size // 2)), mode='constant')\n        output = np.zeros((batch_size, self.output_channels, height, width))\n        for b in range(batch_size):\n            for oc in range(self.output_channels):\n                for r in range(height):\n                    for c in range(width):\n                        # kernel 矩阵和 input 矩阵的, 默认 array1*array2 就是对应元素的乘积\n                        padded_input[b, :, r: r+self.kernel_size, c: c+self.kernel_size] * self.weights[oc, :, :, :]\n                        output[b, oc, r, c] = np.sum(\n                            padded_input[b, :, r:r+self.kernel_size, c:c+self.kernel_size]\n                            * self.weights[oc]) + self.bias[oc]\n        return output\n\nstride = 1\nkernel_size = 3\nfor bs in range(batch_size):\n    for oc in range(output_channels):\n        output[bs, oc, oh, ow] += bias[oc]\n        for ic in range(input_channels):\n            for oh in range(height):\n                for ow in range(width):\n                    for kh in range(kernel_size):\n                        for kw in range(kernel_size):\n                            output[bs, oc, oh, ow] += input[bs, ic, oh+kh, ow+kw] * weights[oc, ic, kh, kw]\nbatch_size = 32\ninput_channels = 3\noutput_channels = 16\nheight = 224\nwidth = 224\nkernel_size = 3\n\ninput = np.random.randn(batch_size, input_channels, height, width)\n\nconv = Conv2D(input_channels, output_channels, kernel_size)\n\nstart_time = time.time()\noutput = conv.forward(input)\nend_time = time.time()\n\nprint(f\"Output shape: {output.shape}\")\nprint(f\"Time taken: {end_time - start_time:.6f} seconds\")\n\nclass Conv2D(torch.nn.Module):\n    def __init__(self, input_channels, output_channels, kernel_size):\n        super().__init__()\n        self.conv = torch.nn.Conv2d(input_channels, output_channels, kernel_size, padding=kernel_size//2)\n\n    def forward(self, x):\n        return self.conv(x)\n\nbatch_size = 32\ninput_channels = 3\noutput_channels = 16\nheight = 224\nwidth = 224\nkernel_size = 3\n\ninput = torch.randn(batch_size, input_channels, height, width)\n\nconv = Conv2D(input_channels, output_channels, kernel_size)\n\nstart_time = time.time()\noutput = conv(input)\nend_time = time.time()\n\nprint(f\"Output shape: {output.shape}\")\nprint(f\"Time taken: {end_time - start_time:.6f} seconds\")\n"
  },
  {
    "path": "1-math_ml_basic/src/python/gd_double_variable.py",
    "content": "# Copyright (c) Microsoft. All rights reserved.\n# Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\ndef target_function(x,y):\n    J = pow(x, 2) + 2*pow(y, 2)\n    return J\n\ndef derivative_function(theta):\n    x = theta[0]\n    y = theta[1]\n    return np.array([2*x, 4*pow(y, 1)])\n\ndef show_3d_surface(x, y, z):\n    fig = plt.figure()\n    ax = Axes3D(fig)\n    u = np.linspace(-3, 3, 100)\n    v = np.linspace(-3, 3, 100)\n    X, Y = np.meshgrid(u, v)\n    R = np.zeros((len(u), len(v)))\n    for i in range(len(u)):\n        for j in range(len(v)):\n            R[i, j] = pow(X[i, j], 2)+ 4*pow(Y[i, j], 2)\n\n    ax.plot_surface(X, Y, R, cmap='rainbow')\n    plt.plot(x, y, z, c='black', linewidth=1.5,  marker='o', linestyle='solid')\n    plt.show()\n\nif __name__ == '__main__':\n    theta = np.array([-3, -3]) # 输入为双变量\n    eta = 0.1 # 学习率\n    error = 5e-3 # 迭代终止条件，目标函数值 < error\n\n    X = []\n    Y = []\n    Z = []\n    for i in range(50):\n        print(theta)\n        x = theta[0]\n        y = theta[1]\n        z = target_function(x,y)\n        X.append(x)\n        Y.append(y)\n        Z.append(z)\n        print(\"%d: x=%f, y=%f, z=%f\" % (i,x,y,z))\n        d_theta = derivative_function(theta)\n        print(\"    \", d_theta)\n        theta = theta - eta * d_theta\n        if z < error:\n            break\n    show_3d_surface(X,Y,Z)"
  },
  {
    "path": "1-math_ml_basic/src/python/gradio_demo.py",
    "content": "import gradio as gr\nimport random\nimport time\n\nwith gr.Blocks() as demo:\n    chatbot = gr.Chatbot()\n    msg = gr.Textbox()\n    clear = gr.Button(\"Clear\")\n\n    def user(user_message, history):\n        return \"\", history + [[user_message, None]]\n\n    def bot(history):\n        bot_message = random.choice([\"Yes\", \"No\"])\n        history[-1][1] = bot_message\n        time.sleep(1)\n        return history\n\n    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(\n        bot, chatbot, chatbot\n    )\n    clear.click(lambda: None, None, chatbot, queue=False)\n\nif __name__ == \"__main__\":\n    demo.launch()"
  },
  {
    "path": "1-math_ml_basic/src/python/multi_cal_tasks.py",
    "content": "# -*- coding  : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n# Create: 2023-03-07\n# Version     : 0.1.0\n# Description: 矩阵计算的 cpu 型密集任务，单进程、多进程和进程池创建指定数量多进程的性能对比\n\nimport threading, multiprocessing\nimport time\nimport numpy as np\nimport concurrent.futures\n\n# 进程数等于CPU核心数\nNUM_PROCESS = multiprocessing.cpu_count()\n\ndef timer(func):\n    \"\"\"decorator: print the cost time of run function\"\"\"\n    def wrapper(*args, **kwargs):\n        start_time = time.time()\n        result = func(*args, **kwargs)\n        end_time = time.time()\n        print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')\n        return result\n    return wrapper\n\n@timer\ndef matrix_mult(A, B):\n    if A.shape[1] != B.shape[0]:\n        raise ValueError(\"Matrix dimensions do not match.\")\n    C = np.zeros((A.shape[0], B.shape[1]))\n    for i in range(A.shape[0]):\n        for j in range(B.shape[1]):\n            for k in range(A.shape[1]):\n                C[i][j] += A[i][k] * B[k][j]\n    return C\n\n@timer\ndef matrix_mult_threaded(A, B, num_threads):\n    if A.shape[1] != B.shape[0]:\n        raise ValueError(\"Matrix dimensions do not match.\")\n    C = np.zeros((A.shape[0], B.shape[1]))\n    def multiply(i_start, i_end):\n        for i in range(i_start, i_end):\n            for j in range(B.shape[1]):\n                for k in range(A.shape[1]):\n                    C[i][j] += A[i][k] * B[k][j]\n    futures = []\n    with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:\n        chunk_size = A.shape[0] // num_threads\n        for i in range(num_threads):\n            i_start = i * chunk_size\n            i_end = (i+1) * chunk_size if i != num_threads-1 else A.shape[0]\n            futures.append(executor.submit(multiply, i_start, i_end))\n        concurrent.futures.wait(futures)\n    return C\n\n@timer\ndef matrix_mult_multiprocess(A, B, num_processes):\n    \"\"\"matrix_mult_multiprocess 是主进程，\n    并通过 multiprocessing.Pool 进程池的方式批量创建子进程。\"\"\"\n    if A.shape[1] != B.shape[0]:\n        raise ValueError(\"Matrix dimensions do not match.\")\n    C = np.zeros((A.shape[0], B.shape[1]))\n    \n    def multiply(i_start, i_end):\n        for i in range(i_start, i_end):\n            for j in range(B.shape[1]):\n                for k in range(A.shape[1]):\n                    C[i][j] += A[i][k] * B[k][j]\n    \n    # Pool 类创建一个进程池，大小默认为 CPU 的核数。\n    p = multiprocessing.Pool()\n    for i in range(num_processes):\n        i_start = i * (A.shape[0] // num_processes)\n        i_end = (i+1) * (A.shape[0] // num_processes) if i != num_processes-1 else A.shape[0]\n        p.apply_async(multiply, args=(i_start, i_end))\n   \n    # print('Waiting for all subprocesses done...')\n    p.close()\n    p.join()\n    return C\n\nif __name__ == \"__main__\":\n    # 测试\n    A = np.random.rand(200, 200)\n    B = np.random.rand(200, 200)\n    C = matrix_mult(A, B)\n    C = matrix_mult_threaded(A, B, NUM_PROCESS)\n    C = matrix_mult_multiprocess(A, B, NUM_PROCESS)\n"
  },
  {
    "path": "1-math_ml_basic/src/python/multi_io_tasks.py",
    "content": "# -*- coding  : utf-8 -*-\n# Author: honggao.zhang + chatgpt\n# Create: 2023-03-07\n# Version     : 0.1.0\n# Description: 下载网页图片的 I/O 型密集任务，单线程、多线程和多进程的性能对比\n\nimport time, os\nimport threading, multiprocessing\nimport queue\nimport requests\n\n# https://picsum.photos/ 网站只需在我们的网址后添加您想要的图片尺寸（宽度和高度），就会得到一张随机图片。\n# 图片的URL列表\nIMAGE_URLS = [\n    \"https://picsum.photos/id/1/200/300\",\n    \"https://picsum.photos/id/2/200/300\",\n    \"https://picsum.photos/id/3/200/300\",\n    \"https://picsum.photos/id/4/200/300\",\n    \"https://picsum.photos/id/5/200/300\",\n    \"https://picsum.photos/id/6/1024/1024\",\n    \"https://picsum.photos/id/7/1024/1024\",\n    \"https://picsum.photos/id/8/1024/1024\",\n    \"https://picsum.photos/id/9/1024/1024\",\n    \"https://picsum.photos/id/10/1024/1024\",\n]\n\n# 存储图片的目录\nSAVE_DIR = \"images/download_images\"\n# 线程数\nTHREAD_POOL_SIZE = multiprocessing.cpu_count()\n# 进程数等于CPU核心数\nNUM_PROCESS = multiprocessing.cpu_count()\n\ndef timer(func):\n    \"\"\"decorator: print the cost time of run function\"\"\"\n    def wrapper(*args, **kwargs):\n        start_time = time.time()\n        result = func(*args, **kwargs)\n        end_time = time.time()\n        print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')\n        return result\n    return wrapper\n\n# 创建存储图片的目录\ndef create_dir(dir_name):\n    if not os.path.exists(dir_name):\n        os.makedirs(dir_name)\n\n# 下载图片的函数\ndef download_image(url, save_path):\n    response = requests.get(url)\n    with open(save_path, \"wb\") as f:\n        f.write(response.content)\n        \n# 单线程下载网页图片\n@timer\ndef single_thread_download():\n    dir_name  = SAVE_DIR + \"_sig_thread\"\n    create_dir(dir_name)\n    for i, url in enumerate(IMAGE_URLS):\n        save_path = os.path.join(dir_name, f\"image{i+1}.jpg\")\n        download_image(url, save_path)\n\n@timer\ndef multi_thread_download():\n    \"\"\"线程数不固定取决于输入图片列表大小\n    \"\"\"\n    dir_name  = SAVE_DIR + \"_muti_thread\"\n    create_dir(dir_name)\n    threads = []\n    for i, url in enumerate(IMAGE_URLS):\n        save_path = os.path.join(dir_name, f\"image{i+1}.jpg\")\n        t = threading.Thread(target=download_image, args=(url, save_path))\n        threads.append(t)\n        t.start()\n    \n    # 等待所有线程执行完毕\n    for t in threads:\n        t.join() \n\ndef thread_worker(work_queue, dir_name):\n    while not work_queue.empty():\n        try:\n            url = work_queue.get(block=False) # 非阻塞取数据\n        except queue.Empty:\n            break\n        else:\n            save_path = os.path.join(dir_name, \"image{}.jpg\".format(url.split('/')[-3]))\n            download_image(url, save_path)\n            # 表示前面排队的任务已经被完成，被队列的消费者线程使用\n            work_queue.task_done()\n\n@timer     \ndef thread_pool_download():\n    # queue 模块实现了多生产者、多消费者队列。适用于消息必须安全地在多线程间交换的多线程编程中。\n    work_queue = queue.Queue()\n    dir_name  = SAVE_DIR + \"_muti_thread_pool\"\n    create_dir(dir_name)\n\n    for i, url in enumerate(IMAGE_URLS):\n        work_queue.put(url)\n    \n    threads = [threading.Thread(target = thread_worker, args=(work_queue, dir_name)) \n               for _ in range(THREAD_POOL_SIZE)]\n    \n    # 启动所有线程\n    for thread in threads:\n        thread.start()\n    # 阻塞至队列中所有的元素都被接收和处理完毕\n    work_queue.join()\n    while threads:\n        threads.pop().join()\n        \ndef process_worker(url):\n    save_path = os.path.join(SAVE_DIR + \"_process_pool\", f\"image{url.split('/')[-3]}.jpg\")\n    download_image(url, save_path)\n    \n@timer\ndef process_pool_download():\n    create_dir(SAVE_DIR + \"_process_pool\")\n\n    # Pool 类创建一个进程池，大小默认为 CPU 的核数。\n    with multiprocessing.Pool() as pool:\n        # 自动将一个可迭代对象（如列表、元组等）中的所有元素分配给多个进程或线程，\n        # 以进行并行计算，然后返回结果列表。它会使进程阻塞直到结果返回。\n        _ = pool.map(process_worker, IMAGE_URLS)\n\nif __name__ == \"__main__\":\n\n    single_thread_download()\n    multi_thread_download()\n    thread_pool_download()\n    process_pool_download()"
  },
  {
    "path": "1-math_ml_basic/src/python/python_basic.py",
    "content": "# 优点1: 生成器可以用于生成无限序列，而列表生成式只能用于有限序列。\ndef fibonacci():\n    prev, curr = 0, 1\n    while True:\n        yield curr\n        prev, curr = curr, prev + curr\n\nfor i, fib in enumerate(fibonacci()):\n    print(\"fib(%d) = %d\" % (i, fib))\n    if i > 10:\n        break\n        \n# 优点2:处理大型数据集时，生成器可以一次生成一个元素，而不是在内存中创建整个列表，\n# 这可以大大减少内存使用。下面是一个使用生成器来读取大型文本文件的伪代码：\n\"\"\"\ndef read_large_file(file_path):\n    with open(file_path) as f:\n        while True:\n            data = f.readline() # 每次读取文件下一行内容\n            if not data:\n                break\n            yield data\n\nfor line in read_large_file(\"large_file.txt\"):\n    print(line)\n    # process_data(line)\n\"\"\"\n\nimport time\n\ndef timer(func):\n    \"\"\"decorator: print the cost time of run function\"\"\"\n    def wrapper(*args, **kwargs):\n        start_time = time.time()\n        result = func(*args, **kwargs)\n        end_time = time.time()\n        print(f'{func.__name__} took {end_time - start_time:.6f} seconds to run')\n        return result\n    return wrapper\n\ndef runTime(func):\n    \"\"\"decorator: print the cost time of run function\"\"\"\n    def wapper(arg, *args, **kwargs):\n        start = time.time()\n        res = func(arg, *args, **kwargs)\n        end = time.time()\n        print(\"=\"*80)\n        print(\"function name: %s\" %func.__name__)\n        print(\"run time: %.4fs\" %(end - start))\n        print(\"=\"*80)\n        return res\n    return wapper\n\n@timer\ndef fib(n):\n    result_list = []\n    prev, curr = 0, 1\n    while n > 0:\n        result_list.append(curr)\n        prev, curr = curr, prev + curr\n        n -= 1\n    return result_list\n    \nresult = fib(300)\n# print(result)\n\nimport functools\n\ndef cache(func):\n    cached_results = {}\n\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        key = str(args) + str(kwargs)\n        if key in cached_results:\n            return cached_results[key]\n        else:\n            result = func(*args, **kwargs)\n            cached_results[key] = result\n            return result\n\n    return wrapper\n\n@cache\ndef fibonacci(n):\n    if n < 2:\n        return n\n    else:\n        return fibonacci(n-1) + fibonacci(n-2)\n\nprint(fibonacci(10))\nprint(fibonacci(20))\n\ndef my_decorator(func):\n    def wrapper(*args, **kwargs):\n        # 在这里可以访问原函数的参数\n        print(\"Function arguments:\", args[0], args[1])\n        result = func(*args, **kwargs)\n        return result\n    return wrapper\n\n@my_decorator\ndef example_function(x, y):\n    return x + y\n\nexample_function(2, 3)\n\n##################### 1，面向对象基础编程-继承和多态 #########################\nimport math\n\nclass Shape:\n    def area(self):\n        pass\n\n    def perimeter(self):\n        pass\n\nclass Rectangle(Shape):\n    def __init__(self, width, height):\n        self.__name__ = 'Rectangle'\n        self.width = width\n        self.height = height\n\n    def area(self):\n        return self.width * self.height\n\n    def perimeter(self):\n        return 2 * (self.width + self.height)\n\nclass Circle(Shape):\n    def __init__(self, radius):\n        self.__name__ = 'Circle'\n        self.radius = radius\n\n    def area(self):\n        return math.pi * self.radius * self.radius\n\n    def perimeter(self):\n        return 2 * math.pi * self.radius\n\ndef calculate(shape):\n    print(\"The class name is type\", shape.__name__)\n    print(f\"Area: {shape.area():.2f}\", shape.area())\n    print(f\"Perimeter: {shape.perimeter():.2f}\", shape.perimeter())\n\nrectangle = Rectangle(5, 10)\ncircle = Circle(5)\n\nshapes = [rectangle, circle]\n\nfor shape in shapes:\n    calculate(shape)\n    \n\"\"\"\nThe class name is type Rectangle\nArea: 50.00 50\nPerimeter: 30.00 30\nThe class name is type Circle\nArea: 78.54 78.53981633974483\n\"\"\"\n\n##################### 2，面向对象基础编程-@property #########################\nclass Person:\n    def __init__(self, age):\n        self._age = age\n        self._age_group = self._calculate_age_group()\n\n    @property\n    def age(self):\n        return self._age\n\n    @age.setter\n    def age(self, value):\n        if value < 0:\n            raise ValueError(\"Age cannot be negative\")\n        elif value > 150:\n            raise ValueError(\"Age is too large\")\n        else:\n            self._age = value\n            self._age_group = self._calculate_age_group()\n\n    @property\n    def age_group(self):\n        return self._age_group\n\n    def _calculate_age_group(self):\n        if self._age < 18:\n            return \"Age range: under 18\"\n        elif self._age < 65:\n            return \"Age range: 18-64\"\n        else:\n            return \"Age range: 65 or over\"\n        \nperson = Person(30)\nprint(person.age,\",\", person.age_group)  # Output: 30, Age range: 18-64\n\n#@property 和 @age.setter 装饰器使得 age 属性看起来像一个普通的属性，但实际上它是一个方法。\nperson.age = 70\nprint(person.age,\",\",person.age_group)  # Output: 70,Age range: 65 or over\n\n# person.age = -10  # Raises ValueError\n\n##################### 3，面向对象基础编程-多重继承 #########################\nclass A:\n    def method1(self):\n        print(\"A method1\")\n\nclass B:\n    def method1(self):\n        print(\"B method1\")\n    def method2(self):\n        print(\"B method2\")\n        \nclass C(A, B):\n    def method3(self):\n        # super() 函数会自动查找方法调用的下一个继承类，并调用该类中的同名方法。\n        super().method1()\n        super().method2()\n        print(\"C method3\")\n\nc = C()\nc.method3() \n\"\"\"\nA method1\nB method2\nC method3\n\"\"\"\n\n##################### 4，面向对象高级编程-定制类 #########################\nclass IntList:\n    def __init__(self, data):\n        self._data = data\n\n    def __len__(self):\n        return len(self._data)\n\n    def __getitem__(self, index):\n        if isinstance(index, int):\n            return self._data[index]\n        elif isinstance(index, slice):\n            return IntList(self._data[index])\n\n    def __setitem__(self, index, value):\n        if isinstance(index, int) and isinstance(value, int):\n            self._data[index] = value\n        elif isinstance(index, slice) and isinstance(value, IntList):\n            self._data[index] = value._data\n\n    def __delitem__(self, index):\n        if isinstance(index, int):\n            del self._data[index]\n        elif isinstance(index, slice):\n            del self._data[index]\n\n    def __contains__(self, item):\n        return item in self._data\n\n    def __iter__(self):\n        return iter(self._data)\n\n    def __repr__(self):\n        return str(self._data)\n\nint_list = IntList([1, 2, 3, 4, 5])\nprint(len(int_list))  # 输出 5\nprint(int_list[0])  # 输出 1\nprint(int_list[1:4])  # 输出 [2, 3, 4]\nint_list[0] = 10\nprint(int_list)  # 输出 [10, 2, 3, 4, 5]\nint_list[1:4] = IntList([20, 30])\nprint(int_list)  # 输出 [10, 20, 30, 5]\ndel int_list[1]\nprint(int_list)  # 输出 [10, 30, 5]\nprint(30 in int_list)  # 输出 True\nfor item in int_list:\n    print(item)  # 依次输出 10, 30, 5\n\n\n##################### 5，面向对象高级编程-元类 #########################\nimport datetime\nclass Meta(type):\n    def __init__(cls, name, bases, attrs):\n        cls.created_at = datetime.datetime.now()\n        super().__init__(name, bases, attrs)\n\nclass MyClass(metaclass=Meta):\n    @property\n    def name(self):\n        return \"MyClass class\"\n\n# 输出 MyClass class create at time: 2023-03-07 00:39:38.628766\nprint(MyClass().name, \"create at time:\", MyClass.created_at)\n\n##################### 6，面向对象编程-实例方法、类方法和静态方法 #########################\n\nclass StringUtils:\n    @staticmethod\n    def reverse_string(string):\n        \"\"\"用于反转给定的字符串\"\"\"\n        return string[::-1]\n\n    @classmethod\n    def count_characters(cls, string):\n        \"\"\"用于计算给定字符串的字符数\"\"\"\n        return len(string)\n\n    def __init__(self, string):\n        self.string = string\n\n    def reverse_instance_string(self):\n        \"\"\"用于反转字符串对象中的字符串\"\"\"\n        return self.string[::-1]\n\n# 使用工具类\nprint(StringUtils.reverse_string(\"Hello, world!\"))\n\nprint(StringUtils.count_characters(\"Hello, world!\"))\n\ns = StringUtils(\"Hello, world!\")\nprint(s.reverse_instance_string())\n\"\"\" \n!dlrow ,olleH\n13\n!dlrow ,olleH\n\"\"\"\n\n#################################7，dataclass 作用#################################\nfrom dataclasses import dataclass\n\n@dataclass\nclass Person:\n    name: str\n    age: int\n    profession: str\n\n# 创建数据类的实例\nperson = Person(\"John Doe\", 30, \"Engineer\")\n\n# 打印数据类的实例\nprint(person)\n\nfrom functools import total_ordering\n\n#################################7，functools#################################\nfrom functools import total_ordering\n\n@total_ordering\nclass Student:\n    def __init__(self, age):\n        self.age = age\n \n    def __lt__(self, other):\n        if isinstance(other, Student):\n            return self.age < other.age\n        else:\n            raise AttributeError(\"Incorrect attribute!\")\n \n    def __eq__(self, other):\n        if isinstance(other, Student):\n            return self.age == other.age\n        else:\n            raise AttributeError(\"Incorrect attribute!\")\n \n \nliming = Student(20)\nlihua = Student(30)\n \nprint(liming < lihua)\nprint(liming <= lihua)\nprint(liming > lihua)\nprint(liming >= lihua)\nprint(liming == lihua)\n\ndef get_dict_depth(d, depth=0):\n    if not isinstance(d, dict):\n        return depth\n    if not d:\n        return depth\n\n    return max(get_dict_depth(v, depth + 1) for v in d.values())\n\n# 测试字典\nmy_dict = {\n    'a': 1,\n    'b': {\n        'c': 2,\n        'd': {\n            'e': 3\n        }\n    }\n}\n\ndepth = get_dict_depth(my_dict)\nprint(\"字典的深度为:\", depth)\n"
  },
  {
    "path": "1-math_ml_basic/src/python/thop_demo.py",
    "content": "import torch\nfrom torchvision.models import resnet50\nfrom thop import profile\nmodel = resnet50()\ninput = torch.randn(1, 3, 224, 224)\nmacs, params = profile(model, inputs=(input, ))\nprint(\"flops: %.2f\\nparameters: %.2f\" % (macs, params))"
  },
  {
    "path": "1-math_ml_basic/ssh远程登录服务.md",
    "content": "`SSH`（安全外壳协议 Secure Shell Protocol，简称SSH）是一种加密的网络传输协议，用于在网络中实现客户端和服务端的连接，典型的如我们在本地电脑通过 `SSH`连接远程服务器，从而做开发，Windows、macOS、Linux都有自带的 `SSH` 客户端，但是在Windows上使用 `SSH` 客户端的体验并不是很好，所以我们一般使用 `Xshell` 来代替。\n\n## 一 准备工作\n\n### 1.1 安装 SSH 客户端\n\n为了建立 SSH 远程连接，需要两个组件：客户端和相应服务端组件，SSH 客户端是我们安装在本地电脑的软件；而服务端，也需有一个称为 SSH 守护程序的组件，它不断地侦听特定的 TCP/IP 端口以获取可能的客户端连接请求。 一旦客户端发起连接，SSH 守护进程将以软件和它支持的协议版本作为响应，两者将交换它们的标识数据。如果提供的凭据正确，SSH 会为适当的环境创建一个新会话。\n\nMacOS 系统自带 SSH 客户端，可以直接使用，Windows 系统需要安装 `Xshell` 客户端软件，大部分 Linux 发行版系统都自带 SSH 客户端，可以直接使用，可通过 `ssh -V` 命令查看当前系统是否有 SSH 客户端。\n\n```bash\n[root@VM-0-2-centos ~]# ssh -V\nOpenSSH_7.4p1, OpenSSL 1.0.2k-fips  26 Jan 2017\n```\n\n### 1.2 安装 SSH 服务端\n\nLinux 系统检查 ssh 服务端是否可用的命令有好几种，比如直接看是否有 `ssh` 进程在运行:\n\n```bash\nps -ef | grep ssh\n```\n\n运行以上后，输出结果示例如下，有 sshd 进程在运行，说明 ssh 服务端可用。\n\n```bash\n-bash-4.3$ ps -e|grep ssh\n  336 ?        00:00:00 sshd\n  358 ?        00:00:00 sshd\n 1202 ?        00:00:00 sshd\n 1978 ?        00:00:00 sshd\n 1980 ?        00:00:00 sshd\n 2710 ?        00:00:00 sshd\n 2744 ?        00:00:00 sshd\n 2829 ?        00:00:00 sshd\n 2831 ?        00:00:00 sshd\n 9864 ?        00:00:00 sshd\n 9893 ?        00:00:02 sshd\n```\n\n对于 Ubuntu 系统，可通过以下命令检查 `OpenSSH` 服务端软件是否可用：\n\n```bash\nssh localhost # 不同 Linux 系统输出可能不一样\n```\n\n## 二 基于密码的登录连接\n\n典型用法，只需输入以下命令即可连接远程服务器。\n    \n```bash\n# ssh连接默认端口是22，如果本地机用户名和远程机用户名一致，可以省略用户名\nssh username@host_ip\n# 也可以指定连接端口\nssh -p port user@host_ip\n```\n\n上述命令是典型的 SSH 连接远程服务器的命令，如果是第一次连接运行后会得到以下提示，正常输入 `yes`，然后输入账号密码即可连接成功:\n    \n```bash\nThe authenticity of host '81.69.58.141 (81.69.58.141)' can't be established.\nED25519 key fingerprint is SHA256:QW5nscbIadeqedp7ByOSUF+Z45rxWGYJvAs3TTmTb0M.\nThis key is not known by any other names\nAre you sure you want to continue connecting (yes/no/[fingerprint])? yes\n\nLast login: Tue Feb 28 15:33:06 2023 from xx.xx.xx.xx\n```\n\n## 三 基于公钥登录连接\n\n前面的命令是通过密码（私钥）登录，这样比较麻烦，因为每次登录我们都需要**输入密码**，因此我们可以选择 SSH 的公钥登录连接方式，省去输入密码的步骤。\n\n公钥登录的原理，是先在本地机器上生成一对**公钥和私钥**，然后手动把公钥上传到远程服务器。这样每次登录时，远程主机会向用户发送一段随机字符串，而用户会用自己的私钥对这段随机字符串进行加密，然后把加密后的字符串发送给远程主机，远程主机会用用户的公钥对这段字符串进行解密，如果解密后的字符串和远程主机发送的随机字符串一致，那么就认为用户是合法的，允许登录。\n只需要把私钥传给远程服务器，远程服务器就可以验证私钥是否是对应的公钥，如果是就允许登录，这样就不需要输入密码了。\n\nSSH 支持多种用于身份验证密钥的公钥算法, 包括 RSA、DSA、ECDSA 和 ED25519 等，其中 RSA 算法是最常用的，因为它是 SSH 协议的默认算法，所以我们这里以 `RSA` 算法为例来生成密钥，并配置免密码远程连接。\n\n`ssh-keygen` 是为 SSH 创建新的身份验证密钥对的工具。此类密钥对用于自动登录、单点登录和验证主机，常用参数定义如下:\n- `-t` 参数指定密钥类型\n- `-b` 参数指定密钥长度\n\n### 3.1 多行命令配置\n\n**基于公钥登录连接（多个命令）的具体步骤如下**:\n\n**本地终端**运行 `ssh-keygen -t rsa -b 4096` 命令生成密钥对，运行后会提示输入密钥保存路径，直接回车即可，保存在默认路径下，然后会提示输入密钥密码，这里我们不设置密码，直接回车即可，然后会提示再次输入密码，这里也不设置密码，直接回车即可，最后会提示密钥生成成功，如下图所示，可以看出 `~/.ssh/` 目录下，会新生成两个文件：`id_rsa.pub` 和 `id_rsa`，分别是公钥和私钥文件。\n\n![ssh-keygen](../images/ssh/ssh_keygen.png)\n\n密钥创建完成后，可手动将本地 `.ssh` 目录下的 `id_rsa.pub` 文件内容添加到目标服务器的 `~/.ssh/authorized_keys` 文件中，如果目标服务器没有 `.ssh` 目录，需要先创建 `.ssh` 目录，然后再创建 `authorized_keys` 文件，然后再添加文件内容。具体操作命令如下:  \n\n```bash\n# 1，本地终端运行命令\ncat ~/.ssh/id_rsa.pub  # 查看本地公钥文件内容，并复制\n# 2，远程终端运行命令，有 authorized_keys 文件则跳过\nmkdir -p ~/.ssh  # 创建 .ssh 目录\ntouch ~/.ssh/authorized_keys  # 创建 authorized_keys 文件\n# 3，然后将本地公钥文件内容粘贴到 `authorized_keys` 文件中，保存退出\n```\n\n上述步骤展开讲是为了让大家理解步骤，实际执行过程，可通过下述 2 条命令自动完成**ssh 基于公钥免输入密码连接远程服务器**:\n\n```bash\nssh-keygen -t rsa -b 4096 # 生成公钥\nssh-copy-id -i ~/.ssh/id_rsa.pub user_name@host_ip # 运行后，它会要求你输入一次服务器密码。\n```\n\n如果上述步骤执行完成后，依然要输入密码完成 ssh 连接，可重点检查目录权限问题，以及不要手动复制公钥文件。\n\n```bash\n# 1. 确保你的家目录只有用户自己能写入\nchmod 755 ~\n\n# 2. 确保 .ssh 目录只有用户自己能进\nchmod 700 ~/.ssh\n\n# 3. 远程主机的的公钥文件只有用户自己能读写\nchmod 600 /home/honggao/.ssh/authorized_keys\n```\n\n<center>\n<img src=\"../images/ssh/ssh_dir_perm.jpg\" width=\"80%\" alt=\"ssh_dir_perm\">\n</center>\n\n### 3.2 一行命令配置\n\n**基于公钥登录连接，也可通过一键完成公钥登录连接的配置命令如下**:\n    \n```bash\n$ ssh username@host \"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys\" < ~/.ssh/id_rsa.pub\n```\n\n只要将公钥文件内容写入远程服务器的 `authorized_keys` 的文件，公钥登录的设置就完成了，后续远程连接就不用每次输入密码了！\n\n`Github` 提交代码的时候，也是通过公钥登录连接的方式，只要将本地的公钥文件内容添加到 github 的 `authorized_keys` 文件中，就可以免密码提交代码了，原理是一模一样的。\n\n## 四 VSCode 远程连接\n\nVSCode 也支持远程连接，可以通过 `Remote-SSH` 插件来实现，具体操作步骤如下:\n\n1，在 VSCode 中安装 [Remote-SSH 插件](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh)。\n\n2，windows 系统 `ctrel + shift + p` 命令打开命令面板，输入 `Remote-SSH: Connect to Host...`，然后选择 `SSH Configuration`，或者通过左侧菜单栏的 `Remote Explorer` -> `SSH Targets` -> `SSH Configuration` 进入。如下图所示:\n\n![vscode-ssh](../images/ssh/remote-ssh.png)\n\n3，然后会打开 `~/.ssh/config` 配置文件，可以参考如下所示模板进行配置:\n\n```bash\n# Read more about SSH config files: https://linux.die.net/man/5/ssh_config\n\nHost JumpMachine\n    HostName xxx.xxx.xxx.xxx\n    # 你跳板机的用户名\n    User username\n\nHost T4\n    # 目标机的ip地址\n    HostName xxx.xxx.xxx.xxx\n    # 你目标机的用户名\n    User username\n    # 目标机登录端口\n    Port 22\n    IdentityFile ~/.ssh/id_rsa\n    # macos系统: ProxyCommand ssh -q -W %h:%p JumpMachine\n    ProxyCommand ssh -q -W %h:%p JumpMachine\n```\n\n4，本地机生产公钥并追加到远程服务器 `authorized_keys` 中的步骤，参考第三章。\n\n5，配置完成后，保存退出，然后在 VSCode 中，点击左侧菜单栏的 `Remote Explorer` -> `SSH Targets` -> `T4`，即可连接到远程服务器。\n\n## 参考资料\n\n1. [维基百科-Secure Shell](https://zh.wikipedia.org/zh-hans/Secure_Shell)\n2. [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen#what-is-ssh-keygen?)\n3. [SSH原理与运用（一）：远程登录](https://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html)"
  },
  {
    "path": "1-math_ml_basic/transformers库快速入门.md",
    "content": "- [一，Transformers 术语](#一transformers-术语)\n  - [1.1，token、tokenization 和 tokenizer](#11tokentokenization-和-tokenizer)\n  - [1.2，input IDs](#12input-ids)\n  - [1.3，attention mask](#13attention-mask)\n  - [1.4，特殊 tokens 的意义](#14特殊-tokens-的意义)\n  - [1.5，decoder models](#15decoder-models)\n  - [1.6，架构与参数](#16架构与参数)\n- [二，Transformers 功能](#二transformers-功能)\n  - [API 概述](#api-概述)\n- [三，快速上手](#三快速上手)\n  - [3.1，transformer 模型类别](#31transformer-模型类别)\n  - [3.2，Pipeline](#32pipeline)\n  - [3.3，AutoClass](#33autoclass)\n    - [3.3.1，AutoTokenizer](#331autotokenizer)\n  - [3.3.2，AutoModel](#332automodel)\n- [参考链接](#参考链接)\n\n## 一，Transformers 术语\n\n### 1.1，token、tokenization 和 tokenizer\n\n`token`: 可以理解为最小语义单元，翻译的话可以是词元、令牌、词，也可以是 word/char/subword，单理解就是单词和标点。\n\n`tokenization`: 是指**分词**过程，目的是将输入序列划分成一个个词元（`token`），保证各个词元拥有相对完整和独立的语义，以供后续任务（比如学习 embedding 或作为 LLM 的输入）使用。\n\n`Tokenizer`: 在 transformers 库中，`tokenizer` 就是实现 `tokenization` 的对象，每个 tokenizer 会有不同的 vocabulary。在代码中，tokenizer 用以将输入文本序列划分成 tokenizer vocabulary 中可用的 `tokens`。\n\n举两个 tokenization 例子：\n\n- “VRAM” 通常不在词汇表中，所以其通常会被划分成 “V”, “RA” and “M” 这样的 `tokens`。\n- 我是中国人->['我', '是', '中国人']\n\n### 1.2，input IDs\n\n`LLM` 唯一必须的输入是 `input ids`，本质是 `tokens` 索引（Indices of input sequence tokens in the vocabulary.），即数整数向量。\n\n- 将输入文本序列转换成 tokens，即 tokenized 过程；\n- 将输入文本序列转换成 input ids，即输入编码过程，数值对应的是 tokenizer 词汇表中的索引，\n\nTransformer 库实现了不同模型的 tokenizer。下面代码展示了将输入序列转换成 tokens 和 input_ids 的结果。\n\n```python\nfrom transformers import BertTokenizer\n\nsequence = \"A Titan RTX has 24GB of VRAM\"\ntokenizer = BertTokenizer.from_pretrained(\"bert-base-multilingual-cased\") \n\ntokenized_sequence = tokenizer.tokenize(sequence) # 将输入序列转换成tokens，tokenized 过程\ninputs = tokenizer(sequence) # 将输入序列转化成符合模型输入要求的 input_ids，编码过程\nencoded_sequence = inputs[\"input_ids\"]\n\nprint(tokenized_sequence)\nprint(encoded_sequence)\nprint(\"[INFO]: length of tokenized_sequence and encoded_sequence:\", len(tokenized_sequence), len(encoded_sequence))\n\n\"\"\"\n['A', 'Titan', 'RT', '##X', 'has', '24', '##GB', 'of', 'VR', '##AM']\n[101, 138, 28318, 56898, 12674, 10393, 10233, 32469, 10108, 74727, 36535, 102]\n[INFO]: length of tokenized_sequence and encoded_sequence: 10 12\n\"\"\"\n```\n\n值得注意的是，**调用 `tokenizer()` 函数返回的是字典对象**，包含相应模型正常工作所需的所有参数，tokens 索引在键 `input_ids` 对应的键值中。同时，**tokenizer 会自动填充 \"special tokens\"**（如果相关模型依赖它们），这也是 tokenized_sequence 和 encoded_sequence 列表中长度不一致的原因。\n\n```python\ndecoded_sequence = tokenizer.decode(encoded_sequence)\nprint(decoded_sequence)\n\"\"\"\n[CLS] A Titan RTX has 24GB of VRAM [SEP]\n\"\"\"\n```\n\n### 1.3，attention mask\n\n注意掩码（`attention mask`）是一个可选参数，一般在将输入序列进行**批处理**时使用。作用是告诉我们哪些 `tokens` 应该被关注，哪些不用。因为如果输入的序列是一个列表，每个序列长度是不一样的，通常是通过填充的方式把他们处理成同一长度。原始 token id 是我们需要关注的，填充的 id 是不用关注的。\n\nattention mask 是二进制张量类型，值为 `1` 的位置索引对应的原始 `token` 表示应该注意的值，而 `0` 表示填充值。\n\n示例代码如下：\n\n```python\nfrom transformers import AutoTokenizer\n\nsentence_list = [\"We are very happy to show you the 🤗 Transformers library.\",\n            \"Deepspeed is faster\"]\nmodel_name = \"nlptown/bert-base-multilingual-uncased-sentiment\"\ntokenizer = AutoTokenizer.from_pretrained(model_name)\n\npadded_sequences = tokenizer(sentence_list, padding=True, return_tensors=\"pt\") # 字典类型\n\nprint(padded_sequences[\"input_ids\"])\nprint(padded_sequences[\"attention_mask\"])\n\n\"\"\"\ntensor([[101, 11312, 10320, 12495, 19308, 10114, 11391, 10855, 10103, 100, 58263, 13299, 119, 102],\n        [101, 15526, 65998, 54436, 10127, 51524, 102, 0, 0, 0, 0, 0, 0, 0]])\ntensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],\n        [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]])\n\"\"\"\n```\n\n![attention_mask](../../images/transformers_basic/attention_mask.png)\n\n### 1.4，特殊 tokens 的意义\n\n我们在模型的 checkpoints 目录下的配置文件中，经常能看到 eop_token、pad_token、bos_token、eos_token 这些与文本序列处理相关的特殊 `token`，它们代表的意义如下:\n\n1. `bos_token`（ Beginning of Sentence Token）：序列开始标记，它表示文本序列的起始位置。\n2. `eos_token`（ End of Sentence Token）：**序列结束标记**，它表示文本序列的结束位置。\n3. `eop_token`（End of Paragraph Token）段落的结束标志，是用于表示段落结束的特殊标记。\n4. `pad_token`（Padding Token）：填充标记，它用于将文本序列填充到相同长度时使用的特殊 token。\n\n### 1.5，decoder models\n\ndecoder 模型也称为自回归（auto-regressive）模型、causal language models，其按顺序阅读输入文本并必须预测下一个单词，在训练中会阅读**添加掩码的句子**。\n\n### 1.6，架构与参数\n\n- **架构**：模型的骨架，包含每个层的类别及定义、各个层的连接方式等等内容。\n- **Checkpoints**：给定架构中会被加载的权重。\n- **模型**：一个笼统的术语，没有“架构”或“参数”那么精确：它可以指两者。\n\n## 二，Transformers 功能\n\n[Transformers](https://github.com/huggingface/transformers) 库提供创建 transformer 模型和加载使用共享模型的功能；另外，[模型中心（hub）](https://huggingface.co/models)包含数千个可以任意下载和使用的预训练模型，也支持用户上传模型到 Hub。\n\n### API 概述\n\n Transformers 库的 `API` 主要包括以下三种：\n\n1. **MAIN CLASSES**：主要包括配置(configuration)、模型(model)、分词器(tokenizer)和流水线(pipeline)这几个最重要的类。\n2. **MODELS**：库中和每个模型实现有关的类和函数。\n3. **INTERNAL HELPERS**：内部使用的工具类和函数。\n\n## 三，快速上手\n\n### 3.1，transformer 模型类别\n\nTransformer 模型架构主要由两个部件组成：\n\n- **Encoder (左侧)**: 编码器接收输入并构建其表示（其特征）。这意味着对模型进行了优化，以从输入中获得理解。\n- **Decoder (右侧)**: 解码器使用编码器的表示（特征）以及其他输入来生成目标序列。这意味着该模型已针对生成输出进行了优化。\n\n![transformer blocks](../../images/transformers_basic/transformers_blocks.svg)\n\n上述两个部件中的每一个都可以作为模型架构独立使用，具体取决于任务：\n\n- **Encoder-only models**: 也叫自动编码 Transformer 模型，如 BERT-like 系列模型，适用于需要理解输入的任务。如句子分类和命名实体识别。\n- **Decoder-only models**: 也叫自回归 Transformer 模型，如 GPT-like 系列模型。适用于生成任务，如**文本生成**。\n- **Encoder-decoder models** 或者 **sequence-to-sequence models**: 也被称作序列到序列的 Transformer 模型，如 BART/T5-like 系列模型。适用于需要根据输入进行生成的任务，如翻译或摘要。\n\n下表总结了目前的 transformers 架构模型类别、示例以及适用任务：\n\n| 模型          | 示例                                       | 任务                                     |\n| ------------- | ------------------------------------------ | ---------------------------------------- |\n| 编码器        | ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa | 句子分类、命名实体识别、从文本中提取答案 |\n| 解码器        | CTRL, GPT, GPT-2, Transformer XL           | 文本生成                                 |\n| 编码器-解码器 | BART, T5, Marian, mBART                    | 文本摘要、翻译、生成问题的回答           |\n\n### 3.2，Pipeline\n\nTransformers 库支持通过 pipeline() 函数设置 `task` 任务类型参数，来跑通不同模型的推理，可实现一行代码跑通跨不同模态的多种任务，其支持的任务列表如下：\n\n| **任务**     | **描述**                                                 | **模态**        | **Pipeline**                                  |\n| ------------ | -------------------------------------------------------- | --------------- | --------------------------------------------- |\n| 文本分类     | 为给定的文本序列分配一个标签                             | NLP             | pipeline(task=\"sentiment-analysis\")           |\n| 文本生成     | 根据给定的提示生成文本                                   | NLP             | pipeline(task=\"text-generation\")              |\n| 命名实体识别 | 为序列里的每个token分配一个标签(人, 组织, 地址等等)      | NLP             | pipeline(task=\"ner\")                          |\n| 问答系统     | 通过给定的上下文和问题, 在文本中提取答案                 | NLP             | pipeline(task=\"question-answering\")           |\n| 掩盖填充     | 预测出正确的在序列中被掩盖的token                        | NLP             | pipeline(task=\"fill-mask\")                    |\n| 文本摘要     | 为文本序列或文档生成总结                                 | NLP             | pipeline(task=\"summarization\")                |\n| 文本翻译     | 将文本从一种语言翻译为另一种语言                         | NLP             | pipeline(task=\"translation\")                  |\n| 图像分类     | 为图像分配一个标签                                       | Computer vision | pipeline(task=\"image-classification\")         |\n| 图像分割     | 为图像中每个独立的像素分配标签(支持语义、全景和实例分割) | Computer vision | pipeline(task=\"image-segmentation\")           |\n| 目标检测     | 预测图像中目标对象的边界框和类别                         | Computer vision | pipeline(task=\"object-detection\")             |\n| 音频分类     | 给音频文件分配一个标签                                   | Audio           | pipeline(task=\"audio-classification\")         |\n| 自动语音识别 | 将音频文件中的语音提取为文本                             | Audio           | pipeline(task=\"automatic-speech-recognition\") |\n| 视觉问答     | 给定一个图像和一个问题，正确地回答有关图像的问题         | Multimodal      | pipeline(task=\"vqa\")                          |\n\n![Hub models](../../images/transformers_basic/transformers_model_hub.png)\n\n\n\n以下代码是通过 pipeline 函数实现对文本的情绪分类。\n\n```python\nfrom transformers import pipeline\n\nclassifier = pipeline(\"sentiment-analysis\")\nprint(classifier(\"I've been waiting for a HuggingFace course my whole life.\"))\n# [{'label': 'POSITIVE', 'score': 0.9598049521446228}]\n```\n\n在 `NLP` 问题中，除了使用 `pipeline()`  任务中默认的模型，也可以通过指定 `model` 和 `tokenizer` 参数来自动查找相关模型。\n\n### 3.3，AutoClass\n\nPipeline() 函数背后实际是通过 “AutoClass” 类，实现**通过预训练模型的名称或路径自动查找其架构**的快捷方式。通过为任务选择合适的 `AutoClass` 和它关联的预处理类，来重现使用 `pipeline()` 的结果。\n\n#### 3.3.1，AutoTokenizer\n\n分词器（`tokenizer`）的作用是负责预处理文本，将输入文本（input prompt）转换为**数字数组**（array of numbers）来作为模型的输入。`tokenization` 过程主要的规则包括：如何拆分单词和什么样级别的单词应该被拆分。值得注意的是，实例化 tokenizer 和 model 必须是同一个模型名称或者 `checkpoints` 路径。\n\n对于 `LLM` ，通常还是使用 `AutoModel` 和 `AutoTokenizer` 来加载预训练模型和它关联的分词器。\n\n```py\nfrom transformers import AutoModel, AutoTokenizer\ntokenizer = LlamaTokenizer.from_pretrained(model_name_or_path)\nmodel = AutoModel.from_pretrained(model_name_or_path, torch_dtype=torch.float16)\n```\n\n一般使用 `AutoTokenizer` 加载分词器（`tokenizer`）:\n\n```python\nfrom transformers import AutoTokenizer\n\nmodel_name = \"nlptown/bert-base-multilingual-uncased-sentiment\"\ntokenizer = AutoTokenizer.from_pretrained(model_name)\n\nencoding = tokenizer(\"We are very happy to show you the 🤗 Transformers library.\")\nprint(encoding)\n\n\"\"\"\n{'input_ids': [101, 11312, 10320, 12495, 19308, 10114, 11391, 10855, 10103, 100, 58263, 13299, 119, 102], \n'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], \n'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}\n\"\"\"\n```\n\n`tokenizer` 的返回是包含了如下“键”的字典：\n\n- [input_ids](https://huggingface.co/docs/transformers/v4.29.1/zh/glossary#input-ids): 用数字表示的 `token`。\n- [attention_mask](https://huggingface.co/docs/transformers/v4.29.1/zh/.glossary#attention-mask): 应该关注哪些 `token` 的指示。\n\ntokenizer() 函数还**支持列表作为输入，并可填充和截断文本, 返回具有统一长度的批次**：\n\n```python\npt_batch = tokenizer(\n    [\"We are very happy to show you the 🤗 Transformers library.\", \"We hope you don't hate it.\"],\n    padding=True,\n    truncation=True,\n    max_length=512,\n    return_tensors=\"pt\",\n)\n```\n\n### 3.3.2，AutoModel\n\nTransformers 提供了一种简单统一的方式来加载预训练的模型实例，即可以像加载 `AutoTokenizer` 一样加载 `AutoModel`，我们所需要提供的必须参数只有模型名称或者 `checkpoints` 路径，即只需输入初始化的 checkpoint(检查点)或者模型名称就可以返回正确的模型体系结构。示例代码如下所示:\n\n```python\nfrom transformers import AutoTokenizer, AutoModelForSequenceClassification\nfrom torch import nn\n\nmodel_name = \"nlptown/bert-base-multilingual-uncased-sentiment\"\ntokenizer = AutoTokenizer.from_pretrained(model_name) # 会下载 vocab.txt 词表\n\n# [\"We are very happy to show you the 🤗 Transformers library.\", \"We hope you don't hate it.\"],\npt_batch = tokenizer(\n    \"We are very happy to show you the 🤗 Transformers library.\", \"We hope you don't hate it.\",\n    padding=True,\n    truncation=True,\n    max_length=512,\n    return_tensors=\"pt\",\n)\n\npt_model = AutoModelForSequenceClassification.from_pretrained(model_name) # 会下载 pytorch_model.bin 模型权重\n\npt_outputs = pt_model(**pt_batch) # ** 可解包 pt_batch 字典\npt_predictions = nn.functional.softmax(pt_outputs.logits, dim=-1) # 在 logits上应用softmax函数来查询概率\n\nprint(pt_predictions)\nprint(pt_model.config.id2label) # {0: '1 star', 1: '2 stars', 2: '3 stars', 3: '4 stars', 4: '5 stars'}\n```\n\n> 注意，Transformers 模型默认情况下是需要多个句子。虽然这里输入看起来是一个句子，但实际 `tokenizer` 不仅将 input ids 列表转换为张量，还在其顶部添加了一个维度（`batch` 维度）。\n\n程序运行结果输出如下所示。\n\n> tensor([[0.0021, 0.0018, 0.0115, 0.2121, 0.7725],\n>         [0.2084, 0.1826, 0.1969, 0.1755, 0.2365]], grad_fn=<SoftmaxBackward0>)\n\n## 参考链接\n\n1. [HuggingFace Transformers 官方文档](https://huggingface.co/docs/transformers/v4.29.1/zh/quicktour)\n2. [NLP Course](https://huggingface.co/learn/nlp-course/zh-CN/chapter1/1)\n3. [NLP领域中的token和tokenization到底指的是什么](https://www.zhihu.com/question/64984731)\n"
  },
  {
    "path": "1-math_ml_basic/深度学习基础-机器学习基本原理.md",
    "content": "---\nlayout: post\ntitle: 深度学习基础-机器学习基本原理\ndate: 2022-11-04 23:00:00\nsummary: 深度学习是机器学习的一个特定分支。我们要想充分理解深度学习，必须对机器学习的基本原理有深刻的理解。大部分机器学习算法都有超参数（必须在学习算法外手动设定）。机器学习本质上属于应用统计学，其更加强调使用计算机对复杂函数进行统计估计。\ncategories: DeepLearning\n---\n\n- [前言](#前言)\n- [5.1 学习算法](#51-学习算法)\n  - [5.1.1 任务 $T$](#511-任务-t)\n  - [5.1.2 性能度量 $P$](#512-性能度量-p)\n  - [5.1.3 经验 $E$](#513-经验-e)\n  - [5.1.4 示例: 线性回归](#514-示例-线性回归)\n- [5.2 容量、过拟合和欠拟合](#52-容量过拟合和欠拟合)\n  - [5.2.1 没有免费午餐定理](#521-没有免费午餐定理)\n  - [5.2.2 正则化](#522-正则化)\n- [5.3 超参数和验证集](#53-超参数和验证集)\n  - [5.3.1 验证集的作用](#531-验证集的作用)\n  - [5.3.2 交叉验证](#532-交叉验证)\n- [5.4 估计、偏差和方差](#54-估计偏差和方差)\n  - [5.4.1 点估计](#541-点估计)\n  - [5.4.2 偏差](#542-偏差)\n  - [5.4.3 方差和标准差](#543-方差和标准差)\n  - [5.4.4 权衡偏差和方差以最小化均方误差](#544-权衡偏差和方差以最小化均方误差)\n- [5.5 最大似然估计](#55-最大似然估计)\n- [5.6 贝叶斯统计](#56-贝叶斯统计)\n- [5.7 监督学习算法](#57-监督学习算法)\n- [5.8 无监督学习算法](#58-无监督学习算法)\n  - [5.8.1 PCA 降维](#581-pca-降维)\n  - [5.8.2 k-均值聚类](#582-k-均值聚类)\n- [5.9 随机梯度下降](#59-随机梯度下降)\n- [5.10 构建机器学习算法 pipeline](#510-构建机器学习算法-pipeline)\n- [参考资料](#参考资料)\n\n> 本文大部分内容参考《深度学习》书籍，从中抽取重要的知识点，并对部分概念和原理加以自己的总结，适合当作原书的补充资料阅读，也可当作快速阅览机器学习原理基础知识的参考资料。\n\n## 前言\n\n**深度学习是机器学习的一个特定分支**。我们要想充分理解深度学习，必须对机器学习的基本原理有深刻的理解。\n\n大部分机器学习算法都有**超参数**（必须在学习算法外**手动设定**）。**机器学习本质上属于应用统计学**，其更加强调使用计算机对复杂函数进行**统计估计**，而较少强调围绕这些函数证明置信区间；因此我们会探讨两种统计学的主要方法: **频率派估计和贝叶斯推断**。同时，大部分机器学习算法又可以分成**监督学习**和**无监督学习**两类；本文会介绍这两类算法定义，并给出每个类别中一些算法示例。\n\n本章内容还会介绍如何组合不同的算法部分，例如优化算法、代价函数、模型和数据 集，来建立一个机器学习算法。最后，在 5.11 节中，我们描述了一些限制传统机器学习泛化能力的因素。正是这些挑战推动了克服这些障碍的深度学习算法的发展。\n> 大部分深度学习算法都是基于随机梯度下降算法进行求解的。\n\n## 5.1 学习算法\n\n机器学习算法是一种能够从数据中学习的算法。这里所谓的“学习“是指：“如果计算机程序在任务 $T$ 中的性能（以 $P$ 衡量）随着经验 $E$ 而提高，则可以说计算机程序从经验 $E$ 中学习某类任务 $T$ 和性能度量 $P$。”-来自 `Mitchell` (`1997`)\n> 经验 $E$，任务 $T$ 和性能度量 $P$ 的定义范围非常宽广，本文不做详细解释。\n\n### 5.1.1 任务 $T$\n\n从 “任务” 的相对正式的定义上说，学习过程本身不能算是任务。学习是我们所谓的获取完成任务的能力。机器学习可以解决很多类型的任务，一些非常常见的机器学习任务列举如下：\n1. **分类**：在这类任务中，计算机程序需要指定某些输入属于 $k$ 类中的哪一类，例如图像分类中的二分类问题，多分类、单标签问题、多分类多标签问题。\n2. **回归**：在这类任务中，计算机程序需要对给定输入预测数值。为了解决这个任务，学习算法需要输出函数 $f : \\mathbb{R}^n \\to \\mathbb{R}$。除了返回结果的形式不一样外，这类 问题和分类问题是很像的。\n3. **机器翻译**\n4. **结构化输出**\n5. **异常检测**\n6. **合成和采样**\n7. **去噪**\n8. **密度估计或概率质量函数估计**\n9. **输入缺失分类**\n10. **转录**\n11. **缺失值填补**\n\n### 5.1.2 性能度量 $P$\n\n为了评估机器学习算法的能力，我们必须设计其性能的**定量度量**，即算法的精度指标。通常，性能度量 $P$ 特定于系统正在执行的任务 $T$。\n> 可以理解为不同的任务有不同的性能度量。\n\n对于诸如分类、缺失输入分类和转录任务，我们通常度量模型的**准确率**(`accu- racy`)。准确率是指该模型输出正确结果的样本比率。我们也可以通过**错误率**(`error rate`)得到相同的信息。错误率是指该模型输出错误结果的样本比率。\n\n我们使用测试集（`test set`）数据来评估系统性能，将其与训练机器学习系统的训练集数据分开。\n\n值得注意的是，性能度量的选择或许看上去简单且客观，但是选择一个与系统理想表现能对应上的性能度量通常是很难的。\n\n### 5.1.3 经验 $E$\n\n根据学习过程中的不同经验，机器学习算法可以大致分类为两类\n\n- **无监督**(`unsuper-vised`)算法\n- **监督**(`supervised`)算法\n\n**无监督学习算法**(`unsupervised learning algorithm`)训练含有很多特征的数据集，然后学习出这个数据集上有用的结构性质。在深度学习中，我们通常要学习生成数据集的整个概率分布，显式地，比如密度估计，或是隐式地，比如合成或去噪。 还有一些其他类型的无监督学习任务，例如聚类，将数据集分成相似样本的集合。\n\n**监督学习算法**(`supervised learning algorithm`)也训练含有很多特征的数据集，但与无监督学习算法不同的是**数据集中的样本都有一个标签**(`label`)或目标(`target`)。例如，`Iris` 数据集注明了每个鸢尾花卉样本属于什么品种。监督学习算法通过研究 `Iris` 数据集，学习如何根据测量结果将样本划分为三个不同品种。\n\n**半监督学习算法**中，一部分样本有监督目标，另外一部分样本则没有。在多实例学习中，样本的整个集合被标记为含有或者不含有该类的样本，但是集合中单独的样本是没有标记的。\n\n大致说来，无监督学习涉及到观察随机向量 $x$ 的好几个样本，试图显式或隐式地学习出概率分布 $p(x)$，或者是该分布一些有意思的性质; 而监督学习包含观察随机向量 $x$ 及其相关联的值或向量 $y$，然后从 $x$ 预测 $y$，通常是估计 $p(y\\vert x)$。术语监督学习(`supervised learning`)源自这样一个视角，教员或者老师提供目标 $y$ 给机器学习系统，指导其应该做什么。在无监督学习中，没有教员或者老师，算法必须学会在没有指导的情况下理解数据。\n\n无监督学习和监督学习并不是严格定义的术语。它们之间界线通常是模糊的。很多机器学习技术可以用于这两个任务。\n\n尽管无监督学习和监督学习并非完全没有交集的正式概念，它们确实有助于粗略分类我们研究机器学习算法时遇到的问题。传统地，人们将回归、分类或者结构化输出问题称为监督学习。支持其他任务的密度估计通常被称为无监督学习。\n\n表示数据集的常用方法是**设计矩阵**(`design matrix`)。\n\n### 5.1.4 示例: 线性回归\n\n我们将机器学习算法定义为，通过经验以提高计算机程序在某些任务上性能的算法。这个定义有点抽象。为了使这个定义更具体点，我们展示一个简单的机器学习示例: **线性回归**(`linear regression`)。\n\n顾名思义，**线性回归解决回归问题**。 换句话说，目标是构建一个系统，该系统可以将向量 $x \\in \\mathbb{R}$ 作为输入，并预测标量 $y \\in \\mathbb{R}$ 作为输出。在线性回归的情况下，输出是输入的线性函数。令 $\\hat{y}$ 表示模型预测值。我们定义输出为\n\n$$\\hat{y} = w^{⊤}x \\tag{5.3}$$\n\n其中 $w \\in \\mathbb{R}^{n}$ 是**参数**(`parameter`)向量。\n\n参数是控制系统行为的值。在这种情况下，$w_i$ 是系数，会和特征 $x_i$ 相乘之 后全部相加起来。我们可以将 $w$ 看作是一组决定每个特征如何影响预测的权重 (weight)。\n\n通过上述描述，我们可以定义任务 $T$ : 通过输出 $\\hat{y} = w^{⊤}x$ 从 $x$ 预测 $y$。\n\n我们使用**测试集**（`test set`）来评估模型性能如何，将输入的设计矩 阵记作 $\\textit{X}$(test)，回归目标向量记作 $y$(test)。\n\n**回归任务**常用的一种模型性能度量方法是计算模型在测试集上的 **均方误差**(`mean squared error`)。如果 $\\hat{y}$(`test`) 表示模型在测试集上的预测值，那么均方误差表示为:\n\n$$MSE_{test} = \\frac{1}{m} \\sum_{i}(\\hat{y}^{(test)}-y^{(test)})_{i}^{2} \\tag{5.4}$$\n\n直观上，当 $\\hat{y}^{(test)}$ = $y^{(test)}$ 时，我们会发现误差降为 0。\n\n图 5.1 展示了线性回归算法的使用示例。\n\n![图5.1-一个线性回归的例子](../images/ml_basc_principle/figure_5_1_an_example_of_linear_regression.png)\n\n## 5.2 容量、过拟合和欠拟合\n\n机器学习的挑战主要在于算法如何在测试集（先前未观测的新输入数据）上表现良好，而不只是在训练集上表现良好，即训练误差和泛化误差读比较小，也可理解为算法泛化性比较好。所谓泛化性（generalized）好指的是，算法在在测试集（以前未观察到的输入）上表现良好。\n\n机器学习算法的两个主要挑战是: **欠拟合**（`underfitting`）和**过拟合**（`overfitting`）。\n- 欠拟合是指模型不能在训练集上获得足够低的误差。\n- 而过拟合是指训练误差和和测试误差之间的差距太大。\n\n通过调整模型的**容量**（`capacity`），我们可以控制模型是否偏向于过拟合或者欠拟合。通俗地讲，**模型的容量是指其拟合各种函数的能力**。容量低的模型可能很难拟合训练集，容量高的模型可能会过拟合，因为记住了不适用于测试集的训练集性质。\n\n一种控制训练算法容量的方法是选择**假设空间**(`hypothesis space`)，即**学习算法可以选择作为解决方案的函数集**。例如，线性回归算法将其输入的所有线性函数的集合作为其假设空间。我们可以推广线性回归以在其假设空间中包含多项式，而不仅仅是线性函数。这样做就增加模型的容量。\n\n> 注意，学习算法的效果不仅很大程度上受影响于假设空间的函数数量，也取决于这些函数的具体形式。\n\n当机器学习算法的容量适合于所执行任务的复杂度和所提供训练数据的数量时，算法效果通常会最佳。容量不足的模型不能解决复杂任务。容量高的模型能够解决复杂的任务，但是当其容量高于任务所需时，有可能会过拟合。\n\n图 5.2 展示了上述原理的使用情况。我们比较了线性，二次和 `9` 次预测器拟合真实二次函数的效果。\n\n![图5-2三个模型拟合二次函数](../images/ml_basc_principle/figure_5_2_three_models_fitting_quadratic_functions.png)\n\n提高机器学习模型泛化性的早期思想是**奥卡姆剃刀**原则，即选择“最简单”的那一个模型。\n\n统计学习理论提供了量化模型容量的不同方法。在这些中，最有名的是 **Vapnik- Chervonenkis 维度**(Vapnik-Chervonenkis dimension, VC)。`VC` 维度量二元分类 器的容量。`VC` 维定义为该分类器能够分类的训练样本的最大数目。假设存在 $m$ 个 不同 $x$ 点的训练集，分类器可以任意地标记该 $m$ 个不同的 $x$ 点，`VC` 维被定义为 $m$ 的最大可能值。\n\n因为可以量化模型的容量，所以使得统计学习理论可以进行量化预测。统计学习理论中最重要的结论阐述了训练误差和泛化误差之间差异的上界随着模型容量增长而增长，但随着训练样本增多而下降 (`Vapnik and Chervonenkis, 1971`; `Vapnik, 1982`; `Blumer et al., 1989`; `Vapnik, 1995`)。这些边界为机器学习算法可以有效解决问题提供了理论 验证，但是它们**很少应用于实际中的深度学习算法**。一部分原因是边界太松，另一部分原因是**很难确定深度学习算法的容量**。由于有效容量受限于优化算法的能力，所以确定深度学习模型容量的问题特别困难。而且我们对深度学习中涉及的非常普遍的**非凸优化问题**的理论了解很少。\n\n虽然更简单的函数更可能泛化(训练误差和测试误差的差距小)，但我们仍然必须选择一个足够复杂的假设来实现低训练误差。通常，随着模型容量的增加，训练误差会减小，直到它逐渐接近最小可能的误差值（假设误差度量具有最小值）。通常，**泛化误差是一个关于模型容量的 U 形曲线函数**。如下图 `5.3` 所示。\n\n![容量和误差之间的典型关系](../images/ml_basc_principle/figure_5_3_model_capacity.png)\n\n### 5.2.1 没有免费午餐定理\n\n机器学习的**没有免费午餐定理**（`Wolpert，1996`）指出，对所有可能的数据生成分布进行平均，每个分类算法在对以前未观察到的点进行分类时具有相同的错误率。换句话说，在某种意义上，**没有任何机器学习算法普遍优于其他任何算法**。\n\n上述这个结论听着真的让人伤感，但庆幸的是，这些结论仅在我们考虑**所有可能的数据生成分布**时才成立。如果我们对实际应用中遇到的概率分布类型做出假设，那么我们可以**设计出在这些分布上表现良好的学习算法**。\n\n这意味着机器学习研究的目标**不是找一个通用学习算法或是绝对最好的学习算法**。反之，我们的目标是理解什么样的分布与人工智能获取经验的 “真实世界” 相关，**什么样的学习算法在我们关注的数据生成分布上效果最好**。\n\n**总结**：没有免费午餐定理清楚地阐述了没有最优的学习算法，即暗示我们必须在特定任务上设计性能良好的机器学习算法。\n\n### 5.2.2 正则化\n\n所谓正则化（Regularization），是指我们通过**修改学习算法，使其降低泛化误差而非训练误差**的方法。\n\n**正则化是一种思想（策略）**，它是机器学习领域的中心问题之一，其重要性只有优化能与其相媲美。\n\n一般正则化一个模型，通常通过对**原始损失函数**引入**额外信息**（也叫惩罚项/正则化项），新的损失函数变成了**原始损失函数 + 惩罚项**对形式。惩罚项通常是对权重参数做一些限制，如 L1/L2 范数：\n\n- L1: $\\lambda \\cdot \\lVert x \\rVert_{1}$，即所有元素的绝对值之和\n- L2: $\\lambda \\cdot \\lVert x \\rVert_{2}$，即所有元素的绝对值平方和\n\n如果正则化项是 $\\Omega(w) = w^{\\top}w$，则称为**权重衰减**（weight decay）。\n\n在标准的随机梯度下降中，权重衰减正则化和 L2 正则化的效果相同。因此，权重衰减在一些深度学习框架中通过 L2 正则化来实现。加入了正则化项后的损失函数的变化如下：\n\n对于均方差损失函数：\n\n$$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}$$\n\n对于交叉熵损失函数：\n\n$$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}$$\n\n其中 $J(w, b)$ 是最终损失函数，$w$ 是权重参数。$\\lambda$ 是正则化项的超参数，需提前设置，**其控制我们对较小权重的偏好强度**。当 $\\lambda = 0$，我们没有任何偏好。$\\lambda$ 越大，则权重越小。最小化 $J(w)$ 会导致权重的选择在**拟合训练数据和较小权重之间进行权衡**。\n> 值得注意的是。在较为复杂的优化方法(比如 Adam)中，权重衰减正则化和 L2 正则化并不等价 [Loshchilov et al., 2017b]。\n\n**和上一节没有最优的学习算法一样，一样的，也没有最优的正则化形式**。反之，我们必须挑选一个非常适合于我们所要解决的任务的正则形式。\n\n那么，**L2 正则化为什么能防止过拟合**呢？\n\n我个人理解是，我们一般默认模型参数越小、模型越简单，越简单的模型越不容易过拟合。而 L2 正则化的模型拟合过程中通常都倾向于让权值尽可能小，即最后能构造出一个所有参数都比较小的模型。\n\n有[文章](https://zhuanlan.zhihu.com/p/41631717)通过可视化加入 L1/L2 正则化后的权重分布，得出 L1/L2 正则化如实地将权重往 `0` 的方向赶，即会让权值尽可能小，但是 L1 赶得快，L2 比较慢一些，在 30 epoc h以上 L2 才有类似于 L1 的一枝独秀的分布。\n\n## 5.3 超参数和验证集\n\n**超参数的值不是通过学习算法本身学习出来的，而是需要算法定义者手动指定的**。\n\n### 5.3.1 验证集的作用\n\n通常，`80%` 的训练数据用于训练，`20%` 用于验证。验证集是用于估计训练中或训练后的泛化误差，从而**更新超参数**。\n\n### 5.3.2 交叉验证\n\n一个**小规模的测试集**意味着平均测试误差估计的统计不确定性，使得很难判断算法 A 是否比算法 B 在给定的任务上做得更好。解决办法是基于在原始数据上**随机采样或分离**出的不同数据集上**重复训练和测试**，最常见的就是 $k$-折交叉验证，即将数据集分成 $k$ 个 不重合的子集。测试误差可以估计为 $k$ 次计算后的平均测试误差。在第 $i$ 次测试时， 数据的第 $i$ 个子集用于测试集，其他的数据用于训练集。算法过程如下所示。\n> k 折交叉验证虽然一定程度上可以解决小数据集上测试误差的不确定性问题，但代价则是增加了计算量。\n\n![k-折交叉验证算法](../images/ml_basc_principle/k-fold.png)\n\n## 5.4 估计、偏差和方差\n\n统计领域为我们提供了很多工具来实现机器学习目标，不仅可以解决训练集上 的任务，还可以泛化。基本的概念，例如参数估计、偏差和方差，对于正式地刻画泛化、欠拟合和过拟合都非常有帮助。\n\n### 5.4.1 点估计\n\n点估计试图为一些感兴趣的量提供单个 ‘‘最优’’ 预测。一般地，感兴趣的量可以是单个参数也可以是一个向量参数，例如第 5.1.4 节线性回归中的权重，但是也有可能是整个函数。\n\n为了区分参数估计和真实值，我们习惯将参数 $\\theta$ 的点估计表示为 $\\hat{\\theta}$。\n\n令 $x^{(1)}, . . . , x^{(m)}$ 是 $m$ 个独立同分布（i.i.d.）的数据点。 点估计(point esti-mator)或统计量(statistics)是这些数据的任意函数:\n\n$$\n\\hat{\\theta_m} =g(x^{(1)},...,x^{(m)}). \\tag{5.19}\n$$\n\n### 5.4.2 偏差\n\n估计的偏差定义如下:\n\n$$\nbias(\\hat{\\theta_m}) = E(\\hat{\\theta_m}) − \\theta, \\tag{5.19}\n$$\n\n其中期望作用在所有数据(看作是从随机变量采样得到的)上，$\\hat{\\theta}$ 是用于定义数据生成分布的 $\\theta$ 的真实值。如果 $bias(\\hat{\\theta_m}) = 0$，那么估计量 $\\hat{\\theta_m}$ 则被称为是**无偏** (unbiased)，同时意味着 $E(\\hat{\\theta_m}) = \\theta$。\n\n### 5.4.3 方差和标准差\n\n方差记为 $Var(\\hat{\\theta})$ 或 $\\sigma^{2}$，方差的平方根被称为标准差。\n\n### 5.4.4 权衡偏差和方差以最小化均方误差\n\n偏差和方差度量着估计量的两个不同误差来源。偏差度量着偏离真实函数或参数的误差期望。而方差度量着数据上任意特定采样可能导致的估计期望的偏差。\n\n**偏差和方差的关系和机器学习容量、欠拟合和过拟合的概念紧密相联**。用 MSE 度量泛化误差(偏差和方差对于泛化误差都是有意义的)时，增加容量会增加方差，降低偏差。如图 5.6 所示，我们再次在关于容量的函数中，看到泛化误差的 U 形曲线。\n\n![偏差方差泛化误差和模型容量的关系](../images/ml_basc_principle/figure_5_6.png)\n\n## 5.5 最大似然估计\n\n与其猜测某个函数可能是一个好的估计器，然后分析它的偏差和方差，我们更希望有一些原则，我们可以从中推导出特定的函数，这些函数是不同模型的良好估计器。最大似然估计就是其中最为常用的准则。\n\n## 5.6 贝叶斯统计\n\n到目前为止，我们已经描述了**频率统计**（frequentist statistics）和基于估计单一 $\\theta$  值的方法，然后基于该估计作所有的预测。 另一种方法是在进行预测时考虑所有可能的 $\\theta$ 值。 后者属于**贝叶斯统计**（Bayesian statistics）的范畴。\n\n如 5.4.1 节中讨论的，频率派的视角是真实参数 $\\theta$  是未知的定值，而点估计  $\\hat{\\theta}$ 是考虑数据集上函数(可以看作是随机的)的随机变量。\n\n贝叶斯统计的视角完全不同。贝叶斯用概率反映知识状态的确定性程度。数据集能够被直接观测到，因此不是随机的。另一方面，真实参数 $\\theta$ 是未知或不确定的， 因此可以表示成随机变量。\n\n## 5.7 监督学习算法\n\n回顾 5.1.3 节内容，简单来说，监督学习算法是给定一组输入 $x$ 和输出 $y$ 的训练 集，学习如何关联输入和输出。在许多情况下，输出 $y$ 可能难以自动收集，必须由人类“监督者”提供，但即使训练集目标是自动收集的，该术语仍然适用。\n\n## 5.8 无监督学习算法\n\n回顾第5.1.3节，无监督算法只处理 “特征’’，不操作监督信号。监督和无监督算法之间的区别没有规范严格的定义，因为没有客观的测试来区分一个值是特征还是监督者提供的目标。通俗地说，无监督学习的大多数尝试是指从不需要人为注释的样本的分布中提取信息。该术语通常与密度估计相关，学习从分布中采样、学习从分布中去噪、寻找数据分布的流形或是将数据中相关的样本聚类。\n\n### 5.8.1 PCA 降维\n\n`PCA`（Principal Component Analysis）是学习数据表示的无监督学习算法，常用于高维数据的降维，可用于提取数据的主要特征分量。\n\nPCA 的数学推导可以从最大可分型和最近重构性两方面进行，前者的优化条件为划分后方差最大，后者的优化条件为点到划分平面距离最小。\n\n### 5.8.2 k-均值聚类\n\n另外一个简单的表示学习算法是 $k$-均值聚类。$k$-均值聚类算法将训练集分成 $k$ 个靠近彼此的不同样本聚类。因此我们可以认为该算法提供了 $k$-维的 one-hot 编码向量 $h$ 以表示输入 $x$。当 $x$ 属于聚类 $i$ 时，有 $h_i = 1$，$h$ 的其他项为零。\n\n$k$-均值聚类初始化 k 个不同的中心点 ${μ^{(1)}, . . . , μ^{(k)}}$，然后迭代交换以下两个不同的步骤直到算法收敛。\n\n1. 步骤一，每个训练样本分配到最近的中心点 $μ^{(i) }$ 所代表的聚类 $i$。 \n2. 步骤二，每一个中心点 $μ^{(i) }$ 更新为聚类 $i$ 中所有训练样本 $x^{(j)}$ 的均值。\n\n关于聚类的一个问题是**聚类问题本身是病态的**。这是说没有单一的标准去度量聚类的数据在真实世界中效果如何。我们可以度量聚类的性质，例如类中元素到类中心点的欧几里得距离的均值。这使我们可以判断从聚类分配中重建训练数据的效果如何。然而我们不知道聚类的性质是否很好地对应到真实世界的性质。此外，可能有许多不同的聚类都能很好地对应到现实世界的某些属性。我们可能希望找到和 一个特征相关的聚类，但是得到了一个和任务无关的，同样是合理的不同聚类。\n\n例如，假设我们在包含红色卡车图片、红色汽车图片、灰色卡车图片和灰色汽车图片的数据集上运行两个聚类算法。如果每个聚类算法聚两类，那么可能一个算法将汽车和卡车各聚一类，另一个根据红色和灰色各聚一类。假设我们还运行了第三个聚类算法，用来决定类别的数目。这有可能聚成了四类，红色卡车、红色汽车、灰色卡 车和灰色汽车。现在这个新的聚类至少抓住了属性的信息，但是丢失了相似性信息。 红色汽车和灰色汽车在不同的类中，正如红色汽车和灰色卡车也在不同的类中。该聚类算法没有告诉我们灰色汽车和红色汽车的相似度比灰色卡车和红色汽车的相似度更高，我们只知道它们是不同的。\n\n## 5.9 随机梯度下降\n\n几乎所有的深度学习算法都用到了一个非常重要的优化算法: **随机梯度下降** (stochastic gradient descent, `SGD`)。\n\n机器学习中反复出现的一个问题是好的泛化需要大的训练集，但大的训练集的 计算代价也更大。\n\n机器学习算法中的代价函数通常可以分解成每个样本的代价函数的总和。\n\n![sgd](../images/ml_basc_principle/sgd.png)\n\n随机梯度下降的核心是，**梯度是期望**，而期望可使用小规模的样本近似估计。具体来说，在算法的每一步，我们从训练集中均匀抽出一小批量(`minibatch`)样本 $B={x^{(1)},...,x^{(m′)}}$。小批量的数目 $m′$ 通常是一个相对较小的数，一般为 $2^n$（取决于显卡显卡）。重要的是，当训练集大小 m 增长时，$m′$ 通常是固定的。我们可能在拟合几十亿的样本时，但每次更新计算只用到几百个样本。\n\n![sgd2](../images/ml_basc_principle/sgd2.png)\n\n梯度下降往往被认为很慢或不可靠。以前，将梯度下降应用到非凸优化问题被认为很鲁莽或没有原则。但现在，我们知道梯度下降用于深度神经网络模型的训练时效果是不错的。优化算法不一定能保证在合理的时间内达到一个局部最小值，但它通常能及时地找到代价函数一个很小的值，并且是有用的。\n\n随机梯度下降在深度学习之外有很多重要的应用。它是在大规模数据上训练大型线性模型的主要方法。对于固定大小的模型，每一步随机梯度下降更新的计算量 不取决于训练集的大小 m。在实践中，当训练集大小增长时，我们通常会使用一个更大的模型，但这并非是必须的。达到收敛所需的更新次数通常会随训练集规模增大而增加。然而，当 m 趋向于无穷大时，该模型最终会在随机梯度下降抽样完训练 集上的所有样本之前收敛到可能的最优测试误差。继续增加 m 不会延长达到模型可能的最优测试误差的时间。从这点来看，我们可以认为用 SGD 训练模型的渐近代价是关于 m 的函数的 $O(1)$ 级别。\n\n## 5.10 构建机器学习算法 pipeline\n\n几乎所有的深度学习算法都可以被描述为一个相当简单的 `pipeline`: \n\n1. 特定的数据集\n2. 代价函数\n3. 优化过程\n4. 神经网络模型。\n\n## 参考资料\n\n- 《深度学习》\n- [【机器学习】降维——PCA（非常详细）](https://zhuanlan.zhihu.com/p/77151308)\n- [更好地理解正则化：可视化模型权重分布](https://zhuanlan.zhihu.com/p/41631717)\n"
  },
  {
    "path": "1-math_ml_basic/深度学习数学基础-概率与信息论.md",
    "content": "---\nlayout: post\ntitle: 深度学习数学基础-概率与信息论\ndate: 2022-11-01 23:00:00\nsummary: 概率论是用于表示不确定性声明的数学框架。它不仅提供了量化不确定性的方法，也提供了用于导出新的不确定性声明（statement）的公理。概率论的知识在机器学习和深度学习领域都有广泛应用，是学习这两门学科的基础。\ncategories: DeepLearning\n---\n\n- [前言](#前言)\n  - [概率论学科定义](#概率论学科定义)\n  - [概率与信息论在人工智能领域的应用](#概率与信息论在人工智能领域的应用)\n- [3.1，为什么要使用概率论](#31为什么要使用概率论)\n- [3.2，随机变量](#32随机变量)\n- [3.3，概率分布](#33概率分布)\n  - [3.3.1，离散型变量和概率质量函数](#331离散型变量和概率质量函数)\n  - [3.3.2，连续型变量和概率密度分布函数](#332连续型变量和概率密度分布函数)\n- [3.4，边缘概率](#34边缘概率)\n- [3.5，条件概率](#35条件概率)\n  - [3.5.1，条件概率的链式法则](#351条件概率的链式法则)\n  - [3.6，独立性和条件独立性](#36独立性和条件独立性)\n- [3.7，条件概率、联合概率和边缘概率总结](#37条件概率联合概率和边缘概率总结)\n- [3.8，期望、方差和协方差](#38期望方差和协方差)\n  - [3.8.1，期望](#381期望)\n    - [期望数学定义](#期望数学定义)\n    - [期望应用](#期望应用)\n    - [总体均值数学定义](#总体均值数学定义)\n  - [3.8.2，方差](#382方差)\n    - [方差数学定义](#方差数学定义)\n    - [总体方差数学定义](#总体方差数学定义)\n  - [3.8.3，期望与方差的运算性质](#383期望与方差的运算性质)\n  - [3.8.4，协方差](#384协方差)\n    - [协方差数学定义](#协方差数学定义)\n- [3.9，常用概率分布](#39常用概率分布)\n  - [3.9.1，伯努利分布](#391伯努利分布)\n  - [3.9.2，Multinoulli 分布](#392multinoulli-分布)\n  - [3.9.3，高斯分布](#393高斯分布)\n  - [3.9.4，指数分布和 Laplace 分布](#394指数分布和-laplace-分布)\n- [3.10，常用函数的有用性质](#310常用函数的有用性质)\n- [3.11，贝叶斯定理](#311贝叶斯定理)\n  - [3.11.1，贝叶斯定理公式](#3111贝叶斯定理公式)\n  - [3.11.2，贝叶斯理论与概率密度函数](#3112贝叶斯理论与概率密度函数)\n- [3.12，连续型变量的技术细节](#312连续型变量的技术细节)\n- [3.13，信息论-相对熵和交叉熵](#313信息论-相对熵和交叉熵)\n- [3.14，结构化概率模型](#314结构化概率模型)\n- [参考资料](#参考资料)\n\n> 本文内容大多来自《深度学习》（花书）第三章概率与信息论。目录的生成是参考此篇 [文章](https://ecotrust-canada.github.io/markdown-toc/)。\n\n## 前言\n\n### 概率论学科定义\n\n概率论是用于表示**不确定性声明的数学框架**。它不仅提供了量化不确定性的方法，也提供了用于导出新的不确定性**声明**（`statement`）的公理。概率论的知识在机器学习和深度学习领域都有广泛应用，是学习这两门学科的基础。\n\n### 概率与信息论在人工智能领域的应用\n\n在人工智能领域，概率论主要有两种用途。\n- 首先，概率定律告诉我们 `AI` 系统应该如何推理，基于此我们设计一些算法来计算或者估算由概率论导出的表达式。\n- 其次，我们可以用概率和统计从理论上分析我们提出的 `AI` 系统的行为。\n\n虽然概率论允许我们在存在不确定性的情况下**做出不确定的陈述和推理**，但信息论允许我们量化概率分布中不确定性的数量。\n\n\n## 3.1，为什么要使用概率论\n\n这是因为机器学习必须始终**处理不确定的量**，有时可能还需要处理随机（非确定性）的量，这里的不确定性和随机性可能来自多个方面。而使用使用概率论来量化不确定性的论据，是来源于 20 世纪 80 年代的 Pearl (1988) 的工作。\n\n不确定性有三种可能的来源:\n1. 被建模系统内在的随机性。\n2. 不完全观测。\n3. 不完全建模：使用了一些必须舍弃某些观测信息的模型。\n\n## 3.2，随机变量\n\n**随机变量**（`random variable`）是可以随机地取不同值的变量，它可以是离散或者连续的。\n\n离散随机变量拥有有限或者可数无限多的状态。注意这些状态不一定非要是整数; 它们也可能只是一些被命名的状态而没有数值。连续随机变量伴随着实数值。注意，随机变量只是对可能状态的描述；它必须与指定这些状态中的每一个的可能性的概率分布相结合。\n\n我们通常用无格式字体 (`plain typeface`) 中的小写字母来表示随机变量本身，而用手写体中的小写字母来表示随机变量能够取到的值。例如， $x_1$ 和 $x_2$ 都是**随机变量** $\\textrm{x}$ 可能的取值。对于向量值变量，我们会将随机变量写成 $\\mathbf{x}$，它的一个可能取值为 $\\boldsymbol{x}$。\n> 中文维基百科用 $X$ 表示随机变量，用 $f_{X}(x)$ 表示概率密度函数，本文笔记，不同小节内容两者混用。\n\n## 3.3，概率分布\n\n**概率分布**（`probability distribution`）是用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。\n\n如果狭义地讲，它是指**随机变量的概率分布函数**。具有相同概率分布函数的随机变量一定是相同分布的。连续型和离散型随机变量的**概率分布描述方式**是不同的。\n\n### 3.3.1，离散型变量和概率质量函数\n\n**离散型变量的概率分布可以用概率质量函数**（`probability mass function`, `PMF`，也称概率密度函数）来描述。我们通常用大写字母 $P$ 来表示概率质量函数，用 $\\textrm{x} \\sim P(\\textrm{x})$ **表示随机变量 $\\textrm{x}$ 遵循的分布**。\n\n虽然通常每一个随机变量都会有一个不同的概率质量函数，但是概率质量函数也可以同时作用于多个随机变量，这种多个变量的概率分布被称为**联合概率分布**（`joint probability distribution`）。 $P(\\textrm{x} = x, \\textrm{y} = y)$ 表示 $\\textrm{x} = x$ 和 $\\textrm{y} = y$ 同时发生的概率，有时也可简写为 $P(x，y)$。\n\n如果一个函数 $P$ 是随机变量 $\\textrm{x}$ 的 `PMF`，必须满足以下条件：\n\n+ $P$ 的定义域必须是 $\\textrm{x}$ 所有可能状态的集合。\n+ $\\forall x \\in \\textrm{x}, 0 \\leq  P(x)\\leq 1$。不可能发生的事件概率为 `0`，能够确保一定发生的事件概率为 `1`。\n+ $\\sum_{x \\in \\textrm{x}}P(x)=1$，**归一化**（`normalized`）。\n\n**常见的离散概率分布族有**：\n+ 伯努利分布\n+ 二项分布：一般用二项分布来计算概率的前提是，每次抽出样品后再放回去，并且只能有两种试验结果，比如黑球或红球，正品或次品等。\n+ 几何分布\n+ `Poisson` 分布（泊松分布）：`Poisson` 近似是二项分布的一种极限形式。\n+ 离散均匀分布：即对于随机变量 $\\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$。\n\n\n### 3.3.2，连续型变量和概率密度分布函数\n\n**连续型随机变量的概率分布可以用概率密度函数**（`probability desity function, PDF`）来描述。\n\n通常用小写字母 $p$ 来表示随机变量 $\\textrm{x}$ 的概率密度函数 `PDF`，其必须满足以下条件：\n+ $p$ 的定义域必须是 $\\textrm{x}$ 所有可能状态的集合。\n+ $\\forall x \\in \\textrm{x}, p(x)\\geq 0$。注意，并不要求 $p(x)\\leq 1$。\n+ $\\int p(x)dx=1$。\n\n概率密度函数 $p(x)$ 给出的是落在面积为 $\\delta x$ 的无限小的区域内的概率为 $p(x)\\delta x$。\n\n因此，我们可以对概率密度函数求积分来获得点集的真实概率质量。特别地，$x$ 落在集合 $\\mathbb{S}$ 中的概率可以通过 $p(x)$ 对这个集合求积分来得到。在单变量的例子中，$x$ 落在区间 $[a,b]$ 的概率是 $\\int_{[a,b]}p(x)dx$。\n\n**常见的连续概率分布族有**：\n+ 均匀分布\n+ **正态分布**：连续型随机变量的概率密度函数如下所示。其密度函数的曲线呈对称钟形，因此又被称之为钟形曲线，其中$\\mu$ 是平均值，$\\sigma$ 是标准差。正态分布是一种理想分布。$${f(x)={\\frac {1}{\\sigma {\\sqrt {2\\pi }}}}e^{\\left(-{\\frac {1}{2}}\\left({\\frac {x-\\mu }{\\sigma }}\\right)^{2}\\right)}}$$\n+ 伽玛分布\n+ 指数分布\n\n## 3.4，边缘概率\n> 边缘概率好像应用并不多，所以这里理解定义和概念即可。\n> 边缘概率的通俗理解描述，来源于 [数学篇 - 概率之联合概率、条件概率、边缘概率和贝叶斯法则(笔记)](https://alili.tech/archive/haz1cu03hf/)。\n\n有时候，我们知道了一组变量的联合概率分布，但想要了解其中一个子集的概率分布。这种定义在子集上的概率分布被称为**边缘概率分布**(`marginal probability distribution`)。\n\n对于离散型随机变量 $\\textrm{x}$ 和 $\\textrm{y}$，知道 $P(\\textrm{x}, \\textrm{y})$，可以依据下面的**求和法则**（`sum rule`）来计算边缘概率 $P(\\textrm{x})$：\n\n$$\\forall x \\in \\textrm{x},P(\\textrm{x}=x)=\\sum_{y}P(\\textrm{x}=x, \\textrm{y}=y)$$\n\n“边缘概率”的名称来源于手算边缘概率的计算过程。当 $P(x,y)$ 的每个值被写在由每行表示不同的 $x$ 值，每列表示不同的 $y$ 值形成的网格中时，对网格中的每行求和是很自然的事情，然后将求和的结果 $P(x)$ 写在每行右边的纸的边缘处。\n\n连续性变量的边缘概率则用积分代替求和：\n\n$$p(x) = \\int p(x,y)dy$$\n\n## 3.5，条件概率\n\n**条件概率（`conditional probability`）就是事件 A 在事件 B 发生的条件下发生的概率**，表示为 $P(A|B)$。\n\n设 $A$ 与 $B$ 为样本空间 Ω 中的两个事件，其中 $P(B)$ > 0。那么在事件 $B$ 发生的条件下，事件 $A$ 发生的条件概率为：\n\n$$P(A|B)={\\frac {P(A\\cap B)}{P(B)}}$$\n\n> 花书中期望的条件概率定义（表达式不一样，但意义是一样的，维基百科的定义更容易理解名字意义，花书中的公式更多的是从数学中表达）:\n\n> 将给定 $\\textrm{x} = x$ 时， $\\textrm{y} = y$ 发生的条件概率记为 $P(\\textrm{y} = y|\\textrm{x} = x)$，这个条件概率的计算公式如下：\n> $$P(\\textrm{y}=y|\\textrm{x}=x)=\\frac{P(\\textrm{y}=y, \\textrm{x}=x)}{P(\\textrm{x}=x)}$$\n> 条件概率只在 $P(\\textrm{x}=x)\\geq 0$ 时有定义，即不能计算以从未发生的事件为条件的条件概率。\n\n### 3.5.1，条件概率的链式法则\n\n任何多维随机变量的联合概率分布，都可以分解成只有一个变量的条件概率相乘的形式，这个规则被称为概率的**链式法则**（`chain rule`）。条件概率的链式法则如下:\n\n$$\n\\begin{align}\nP(a,b,c) &= P(a|b,c)P(b,c) \\nonumber \\\\\nP(b,c) &= P(b|c)P(c) \\nonumber \\\\\nP(a,b,c) &= P(s|b,c)P(b|c)P(c) \\nonumber\n\\end{align}\n$$\n\n### 3.6，独立性和条件独立性\n\n两个随机变量 $\\textrm{x}$ 和 $\\textrm{y}$，如果它们的概率分布可以表示成两个因子的乘积形式，并且一个因子只包含 $\\textrm{x}$ 另一个因子只包含 $\\textrm{y}$，我们就称这两个随机变量是**相互独立**的（`independent`）：\n\n$$\\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)$$\n\n两个相互独立的随机变量同时发生的概率可以通过各自发生的概率的乘积得到。\n\n如果关于 $x$ 和 $y$ 的条件概率分布对于 $z$ 的每一个值都可以写成乘积的形式，那么这两个随机变量 $x$ 和 $y$ 在给定随机变量 $z$ 时是条件独立的(conditionally independent):\n\n$$\\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})$$\n\n采用一种简化形式来表示独立性和条件独立性: $\\textrm{x}\\perp\\textrm{y}$ 表示 $\\textrm{x}$ 和 $\\textrm{y}$ 相互独立，$\\textrm{x}\\perp\\textrm{y}\\vert\\textrm{z}$ 表示 $\\textrm{x}$ 和 $\\textrm{y}$ 在给定 $\\textrm{z}$ 时条件独立。\n\n## 3.7，条件概率、联合概率和边缘概率总结\n\n1. **条件概率（`conditional probability`）就是事件 A 在事件 B 发生的条件下发生的概率**。条件概率表示为 $P(A\\vert B)$，读作“A 在 B 发生的条件下发生的概率”。\n2. 联合概率表示两个事件共同发生的概率。`A` 与 `B` 的联合概率表示为 $P(A\\cap B)$ 或者 $P(A,B)$ 或者 $P(AB)$。\n3. 仅与单个随机变量有关的概率称为边缘概率。\n\n## 3.8，期望、方差和协方差\n> 为了便于理解，本章中的期望和方差的数学定义主要采用中文维基百科中的定义。\n\n在概率分布中，期望值和方差或标准差是一种分布的重要特征，期望、数学期望、均值都是一个意思。统计中的方差（样本方差）是每个样本值与全体样本值的平均数之差的平方值的平均数，其意义和概率分布中的方差是不一样的。\n\n### 3.8.1，期望\n\n在概率论和统计学中，一个离散性随机变量的期望值（或数学期望，亦简称期望，物理学中称为期待值）是试验中**每次可能的结果乘以其结果概率的总和**。换句话说，期望值像是随机试验在同样的机会下重复多次，所有那些可能状态平均的结果，也可理解为**该变量输出值的加权平均**。\n\n#### 期望数学定义\n\n如果 $X$ 是在概率空间 $(\\Omega ,F,P)$ 中的随机变量，那么它的期望值 $\\operatorname{E}(X)$ 的定义是：\n\n$$\\operatorname {E}(X)=\\int_{\\Omega }X {d}P$$\n\n**并不是每一个随机变量都有期望值的，因为有的时候上述积分不存在。如果两个随机变量的分布相同，则它们的期望值也相同**。\n\n1，如果 $X$ 是**离散的随机变量**，输出值为 $x_{1},x_{2},\\ldots x_{1},x_{2},\\ldots$，和输出值相应的概率为 ${\\displaystyle p_{1},p_{2},\\ldots }p_{1},p_{2},\\ldots$（概率和为 `1`）。\n\n若级数 $\\sum_{i}p_{i}x_{i}$ 绝对收敛，那么期望值 $\\operatorname {E}(X)$ 是一个无限数列的和。\n\n$$\\operatorname {E}(X)=\\sum_{i}p_{i}x_{i}$$\n\n2，如果 $X$ 是**连续的随机变量**，且存在一个相应的概率密度函数 $f(x)$，若积分 $\\int _{-\\infty }^{\\infty }xf(x)\\,\\mathrm {d} x$ 绝对收敛，那么 $X$ 的期望值可以计算为：\n\n$$\\operatorname {E} (X)=\\int _{-\\infty }^{\\infty }xf(x)\\,\\mathrm {d} x$$\n\n虽然是针对于连续的随机变量的，但与离散随机变量的期望值的计算算法却同出一辙，由于输出值是连续的，所以**只是把求和改成了积分**。\n\n**期望值 $E$ 是线性函数**:\n\n$$\\operatorname {E}(aX+bY)=a\\operatorname {E}(X)+b\\operatorname {E}(Y)$$\n\n$X$ 和 $Y$ 为**在同一概率空间的两个随机变量**（可以独立或者非独立），$a$ 和 $b$ 为任意实数。\n\n> 花书中期望的数学定义（表达式不一样，但意义是一样的）:\n>\n> 1，某个函数 $f(x)$ 相对于概率分布 $P(x)$ 的期望（期望值）是当从 $P$ 中抽取 $x$ 时 $f$ 所取的平均或平均值。对于离散型随机变量，期望可以通过**求和**得到：\n> $$\\mathbb{E}_{\\textrm{x}\\sim P}[f(x)] = \\sum_{x} P(x)f(x)$$\n>\n> 2，对于连续型随机变量可以通过求**积分**得到：\n> $$\\mathbb {E}_{\\textrm{x}\\sim p}[f(x)] = \\int p(x)f(x)dx$$\n\n#### 期望应用\n\n1. 在**统计学**中，估算变量的期望值时，经常用到的方法是重复测量此变量的值，再用所得数据的平均值来估计此变量的期望值。\n2. 在**概率分布**中，期望值和方差或标准差是一种分布的重要特征。\n\n#### 总体均值数学定义\n\n一般而言，一个有限的容量为 $N$、元素的值为 $x_{i}$ 的总体的总体均值为：\n\n$$\\mu = \\frac{\\sum_i^N x_{i}}{N}$$\n\n### 3.8.2，方差\n\n在概率论和统计学中，方差（英语：`variance`）又称变异数、变方，描述的是**一个随机变量的离散程度，即该变量离其期望值的距离**，是随机变量与其总体均值或样本均值的离差的平方的期望值。\n\n方差差是标准差的平方、分布的二阶矩，以及随机变量与其自身的协方差，其常用的符号表示有 $\\sigma^2$、$s^2$、$\\operatorname {Var} (X)$、$\\displaystyle V(X)$，以及 $\\displaystyle \\mathbb {V} (X)$。\n\n方差作为离散度量的优点是，它比其他离散度量（如平均差）更易于代数运算，但缺点是它与随机变量的单位不同，而标准差则单位相同，这就是计算完成后通常采用标准差来衡量离散程度的原因。\n> 方差的正平方根称为该随机变量的标准差。\n\n有两个不同的概念都被称为“方差”。一种如上所述，是理论概率分布的方差。而另一种方差是一组观测值的特征，分别是**总体方差**（所有可能的观测）和**样本方差**（总体的一个子集）。\n\n#### 方差数学定义\n\n设 $X$ 为服从分布 $F$ 的随机变量，如果 $\\operatorname{E}[X]$ 是随机变量 $X$ 的期望值（均值 $\\mu=\\operatorname{E}[X]$），则随机变量 $X$ 或者分布 $F$ 的**方差**为 $X$ 的**离差平方的期望值**:\n\n$$\\operatorname{E}(X) = \\operatorname{E}[(X - \\mu)]^2 = \\operatorname{E}[X - \\operatorname{E}(X)]^2$$\n\n方差的表达式可展开如下：\n\n$$\n\\begin{align}\n\\operatorname{Var}(X) &=\\operatorname{E} \\left[(X-\\operatorname {E} [X])^{2}\\right] \\nonumber \\\\\n&=\\operatorname{E} \\left[X^{2}-2X\\operatorname {E} [X]+\\operatorname{E}[X]^{2}\\right] \\nonumber \\\\\n&=\\operatorname{E} \\left[X^{2}\\right]-2\\operatorname{E}[X]\\operatorname{E}[X]+\\operatorname{E}[X]^{2} \\nonumber \\\\\n&=\\operatorname{E} \\left[X^{2}\\right]-\\operatorname{E}[X]^{2} \\nonumber \\\\\n\\end{align}\n$$\n\n也就是说，$X$ 的方差等于 $X$ 平方的均值减去 $X$ 均值的平方。\n\n#### 总体方差数学定义\n\n一般而言，一个有限的容量为 $N$、元素的值为 $x_{i}$ 的总体的总体方差为：\n\n$$\\sigma^{2} = {\\frac {1}{N}}\\sum _{i=1}^{N}\\left(x_{i}-\\mu \\right)^{2}$$\n\n> 花书中方差的定义: **方差**（`variance`）衡量的是当我们对 $x$ 依据它的概率分布进行采样时，随机变量 $\\textrm{x}$ 的函数值会呈现多大的差异，或者说一个随机变量的方差描述的是它的离散程度，也就是该变量离其期望值的距离。方差定义如下：\n> $$Var(f(x)) = \\mathbb{E}[(f(x) - \\mathbb{E}[f(x)])^2]$$\n\n### 3.8.3，期望与方差的运算性质\n\n**期望与方差运算性质**如下:\n\n![期望运算性质](../images/ml/expected_properties.png)\n\n![方差运算性质](../images/ml/variance_operation_properties.png)\n> 来源: 知乎文章-[【AP统计】期望E(X)与方差Var(X)](https://zhuanlan.zhihu.com/p/64859161)。\n\n### 3.8.4，协方差\n\n协方差也叫共变异数（英语：Covariance），在概率论与统计学中用于**衡量两个随机变量的联合变化程度**。\n\n#### 协方差数学定义\n\n期望值分别为 $\\operatorname E(X)=\\mu$ 与 $\\operatorname E(Y)=\\nu$ 的两个具有有限二阶矩的实数随机变量 $X$ 与 $Y$ 之间的协方差定义为：\n\n$$\\operatorname {cov} (X,Y)=\\operatorname {E} ((X-\\mu )(Y-\\nu ))=\\operatorname {E} (X\\cdot Y)-\\mu \\nu$$\n\n协方差表示的是两个变量的总体的误差，这与只表示一个变量误差的方差不同。\n\n协方差的绝对值如果很大则意味着变量值变化很大并且它们同时距离各自的均值很 远。如果协方差是正的，那么两个变量都倾向于同时取得相对较大的值。如果协方 差是负的，那么其中一个变量倾向于取得相对较大的值的同时，另一个变量倾向于 取得相对较小的值，反之亦然。其他的衡量指标如 相关系数(`correlation`)将每个变 量的贡献归一化，为了只衡量变量的相关性而不受各个变量尺度大小的影响。\n\n## 3.9，常用概率分布\n\n下表列出了一些常用概率分布的方差。\n\n![probability_distributions](../images/dl/probability_distributions.png)\n\n### 3.9.1，伯努利分布\n\n**伯努利分布**（英语：`Bernoulli distribution`），又名两点分布或者 `0-1` 分布，是一个离散型概率分布，为纪念瑞士科学家雅各布·伯努利而命名。若伯努利试验成功，则伯努利随机变量取值为 `1`。若伯努利试验失败，则伯努利随机变量取值为 `0`。记其成功概率为 $0\\leq p\\leq 1$，失败概率为 $q = 1-p$。其有如下性质:\n\n1. 其**概率质量函数**为:\n\n$$\nf_{X}(x) = p^{x}(1-p)^{1-x} = \\left\\lbrace\\begin{matrix}\np \\quad if \\;x = 1 \\\\ \n1-p \\quad  if \\; x = 0\n\\end{matrix}\\right.\n$$\n\n2. 其**期望值**为: \n$$\\operatorname {E} [X] = \\sum_{i=0}^{1} x_{i}f_X(x) = 0 + p = p$$\n3. 其**方差**为:\n\n$$\\begin{aligned}\nVar[X] &= \\sum_{i=0}^{1} (x_{i}-\\operatorname {E} [X])^2f_{X}(x) \\\\\n&= (0-P)^2(1-P) + (1-P)^2P \\\\\n&= p(1-p) \\\\\n&= p\\cdot q \\\\\n\\end{aligned}$$\n\n### 3.9.2，Multinoulli 分布\n\n`Multinoulli` 分布(多项式分布，也叫范畴分布 `categorical dis- tribution`)是一种离散概率分布，它描述了随机变量的可能结果，该随机变量可以采用 $k$ 个可能类别之一，概率为每个类别分别指定，其中 $k$ 是一个有限值。\n\n### 3.9.3，高斯分布\n> 有几种不同的方法用来说明一个随机变量。最直观的方法是**概率密度函数**，这种方法能够表示随机变量每个取值有多大的可能性。\n\n高斯分布 `Gaussian distribution`（也称正态分布 `Normal distribution`）是一个非常常见的**连续概率分布**。高斯分布在统计学上十分重要，经常用在自然和社会科学来代表一个不确定的随机变量。\n\n若随机变量 $X$ 服从一个位置参数为 $\\mu$ 、尺度参数为 $\\sigma$ 的正态分布，记为：\n\n$$X \\sim N(\\mu,\\sigma^2)$$\n\n则其**概率密度函数**为 $$f(x;\\mu, \\sigma) = \\frac {1}{\\sigma {\\sqrt {2\\pi }}}\\;e^{-{\\frac {\\left(x-\\mu \\right)^{2}}{2\\sigma ^{2}}}}$$\n\n正态分布的数学期望值 $\\mu$ 等于位置参数，决定了分布的**位置**；其方差 $\\sigma^2$ 的开平方或标准差 $\\sigma$ 等于尺度参数，决定了分布的**幅度**。\n\n正态分布概率密度函数曲线呈钟形，也称之为钟形曲线（类似于寺庙里的大钟，因此得名）。我们通常所说的**标准常态分布**是位置参数 $\\mu = 0$，尺度参数 $\\sigma ^{2} = 1$ 的正态分布（见右图中红色曲线）。\n\n![四个不同参数集的概率密度函数（红色线代表标准正态分布）](../images/dl/2880px-Normal_Distribution_PDF.png)\n\n采用正态分布在很多应用中都是一个明智的选择。当我们由于缺乏关于某个实 数上分布的先验知识而不知道该选择怎样的形式时，正态分布是默认的比较好的选择，其中有两个原因。\n\n1. 第一，我们想要建模的很多分布的真实情况是比较接近正态分布的。\n2. 第二，在具有相同方差的所有可能的概率分布中，正态分布在实数上具有最 的不确定性。因此，我们可以认为正态分布是对模型加入的先验知识量最少的分布。\n\n### 3.9.4，指数分布和 Laplace 分布\n\n在概率论和统计学中，**指数分布**（`Exponential distribution`）是一种连续概率分布，表示一个在 $x = 0$ 点处取得边界点 (`sharp point`) 的分布，其使用指示函数(`indicator function`) $1_{x\\geq0}$ 来使得当 $x$ 取负值时的概率为零。指数分布可以等同于形状母数 $\\alpha$为 $1$的**伽玛分布**。\n\n指数分布可以用来表示独立随机事件发生的时间间隔，比如旅客进入机场的时间间隔、电话打进客服中心的时间间隔等。\n\n若随机变量 $X$ 服从母数为 $\\lambda$ 或 $\\beta$ 的指数分布，则记作\n\n$X\\sim {\\text{Exp}}(\\lambda )$ 或 $X\\sim {\\text{Exp}}(\\beta )$\n\n两者意义相同，只是 $\\lambda$ 与 $\\beta$ 互为倒数关系。**指数分布的概率密度函数**为：\n\n$$\nf(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.\n$$\n\n**指数分配概率密度函数曲线**如下所示。\n\n![指数分配概率密度函数](../images/dl/Exponential_distribution_pdf.png)\n\n## 3.10，常用函数的有用性质\n\n深度学习中的概率分布有一些经常出现的函数，比如 `logistic sigmoid` 函数:\n\n$$\\sigma(x) = \\frac{1}{1+exp(-x)}$$\n\n`logistic sigmoid` 函数通常用来产生伯努利分布的参数 $p$，因为它的范围是 $(0, 1)$，位于 $p$ 参数值的有效范围内。下图 3.3 给出了 `sigmoid` 函数的图示。从图中可以明显看出，`sigmoid` 函数在变量取绝对值非常大的正值或负值时会出现**饱和(`saturate`)现象**，意味着函数会变得**很平**，并且对输入的微小改变会变得**不敏感**。\n\n![sigmoid函数示意图](../images/dl/schematic_diagram_of_sigmoid_function.png)\n\n`sigmoid` 函数的一些性质在后续学习 `BP` 算法等内容时会很有用，我们需要牢记：\n\n$$\n\\begin{align}\n\\sigma(x) &= \\frac{exp(x)}{exp(x)+exp(0)} \\nonumber \\\\\n\\frac{d}{dx}\\sigma(x) &= \\sigma(x)(1 - \\sigma(x)) \\nonumber \\\\\n1 - \\sigma(x) &= \\sigma(-x) \\nonumber \\\\\n\\end{align}\n$$\n\n## 3.11，贝叶斯定理\n> 本小节只是简单介绍基本概念和公式，更全面和深入的理解建议看《机器学习》书籍。\n\n贝叶斯定理（英语：`Bayes' theorem`）是概率论中的一个定理，描述**在已知一些条件下，某事件的发生概率**。比如，如果已知某种健康问题与寿命有关，使用贝叶斯定理则可以通过得知某人年龄，来更加准确地计算出某人有某种健康问题的概率。\n\n通常，事件 A 在事件 B 已发生的条件下发生的概率，与事件 B 在事件 A 已发生的条件下发生的概率是不一样的。但是，这两者是有确定的关系的，贝叶斯定理就是这种关系的陈述。贝叶斯公式的一个用途，即透过已知的三个概率而推出第四个概率。贝叶斯定理跟随机变量的条件概率以及边际概率分布有关。\n\n作为一个普遍的原理，贝叶斯定理对于所有概率的解释是有效的。这一定理的主要应用为贝叶斯推断，是推论统计学中的一种推断法。这一定理名称来自于托马斯·贝叶斯。\n\n> 来源[中文维基百科-贝叶斯定理](https://zh.wikipedia.org/wiki/%E8%B4%9D%E5%8F%B6%E6%96%AF%E5%AE%9A%E7%90%86)\n\n### 3.11.1，贝叶斯定理公式\n\n贝叶斯定理是关于随机事件 A 和 B 的条件概率的一则定理。\n\n$$P(A\\mid B)={\\frac {P(A)P(B\\mid A)}{P(B)}}$$\n\n其中 A 以及 B 为随机事件，且 $P(B)$ 不为零。$P(A\\mid B)$ 是指在事件 B 发生的情况下事件 A 发生的概率。\n\n在贝叶斯定理中，每个名词都有约定俗成的名称：\n- $P(A\\mid B)$ 是已知 B 发生后，A 的**条件概率**。也称作 A 的事后概率。\n- $P(A)$ 是 A 的先验概率（或边缘概率）。其不考虑任何 B 方面的因素。\n- $P(B\\mid A)$ 是已知 A 发生后，B 的条件概率。也可称为 B 的后验概率。某些文献又称其为在特定 B 时，A 的似然性，因为 $P(B\\mid A)=L(A\\mid B)$。\n- $P(B)$是 B 的**先验概率**。\n\n### 3.11.2，贝叶斯理论与概率密度函数\n\n贝叶斯理论亦可用于概率分布，贝叶斯理论与概率密度的关系是由求极限的方式建立：\n\n$$P(\\textrm{x}|\\textrm{y}) = \\frac{P(\\textrm{x})P(\\textrm{y}|\\textrm{x})}{P(\\textrm{y})}$$\n\n注意到 $P(y)$ 出现在上面的公式中，它通常使用 $P(\\textrm{y}) = \\sum_{x} P(\\textrm{y}|x)P(x)$ 来计算所以我们并不需要事先知道 $P(\\textrm{y})$ 的信息。\n\n> 中文维基百科中贝叶斯理论与概率密度关系定义:\n> $$f(x|y)={\\frac {f(x,y)}{f(y)}}={\\frac {f(y|x)\\,f(x)}{f(y)}}$$\n\n## 3.12，连续型变量的技术细节\n\n连续型随机变量和概率密度函数的深入理解需要用到数学分支**测度论**(`measure theory`)的相关内容来扩展概率论，测度论超出了本文范畴。《深度学习》原书中有测度论的简要介绍，本笔记不做记录和摘抄，感兴趣的可以阅读原书。\n\n## 3.13，信息论-相对熵和交叉熵\n\n信息论是应用数学、电子学和计算机科学的一个分支，早期备用在无线通信领域。在深度学习中，主要是使用信息论的一些关键思想来**表征(`characterize`)概率分布**或者**量化概率分布之间的相似性**。\n\n信息论的基本想法是一个不太可能的事件居然发生了，要比一个非常可能的事件发生，能提供更多的信息。\n\n定义一个事件 $\\textrm{x} = x$ 的自信息(self-information) 为\n\n$$\nI(x) = -\\text{log}P(x)\n$$\n\n在本文中，我们总是用 $\\text{log}$ 来表示自然对数，其底数为 $e$。因此我们定义的 $I(x)$ 单位是**奈特**(nats)。一奈特是以 $\\frac{1}{e}$ 的概率观测到一个事件时获得的信息量。其他的材料中可能使用底数为 2 的对数，单位是**比特**(bit)或者香农(shannons); 通过比特度量的信息只是通过奈特度量信息的常数倍。\n\n自信息只处理单个的输出。我们可以用**香农熵**(`Shannon entropy`)来对整个概率分布中的不确定性总量进行量化:\n\n$$H(P) = H(\\textrm{x}) = E_{x∼P}[I(x)] = −E_{x∼P}[log P(x)]$$\n\n换句话说，一个概率分布的香农熵是指遵循这个分布的事件所产生的期望信息总量。\n\n如果我们对于同一个随机变量 $\\textrm{x}$ 有两个单独的概率分布 $P(\\textrm{x})$ 和 $Q(\\textrm{x})$，则可以用 **KL 散度**（ `Kullback-Leibler (KL) divergence`，也叫相对熵）来**衡量这两个概率分布的差异**：\n\n$$D_{KL}(P\\parallel Q) = \\mathbb{E}_{\\textrm{x}\\sim p}\\begin{bmatrix}\nlog \\frac{P(x)}{Q(x)} \\end{bmatrix} = \\mathbb{E}_{\\textrm{x}\\sim p}[log P(x) - log Q(x)]$$\n\nKL 散度有很多有用的性质，最重要的是它是非负的。KL 散度为 0 当且仅当 $P$ 和 $Q$ 在离散型变量的情况下是相同的概率分布，或者在连续型变量的情况下是 “几乎处处” 相同的。\n\n一个和 KL 散度密切联系的量是**交叉熵**(`cross-entropy`)$H(P, Q) = H(P) + D_{KL}(P\\vert\\vert Q)$，其计算公式如下:\n\n$$H(P, Q)  = -\\mathbb{E}_{\\textrm{x}\\sim p}log Q(x)$$\n\n和 KL 散度相比，少了左边一项，即熵 $H(P)$。可以看出，最小化 KL 散度其实就是在最小化分布之间的交叉熵。\n\n> 上式的写法是在前面所学内容**数学期望**的基础上给出的，还有一个写法是《机器学习-周志华》书中附录 C 中给出的公式，更为直观理解：\n> $$KL(P\\parallel Q) = \\int_{-\\infty }^{+\\infty} p(x)log \\frac{p(x)}{q(x)} dx$$\n> 其中 $p(x)$ 和 $q(x)$ 分别为 $P$ 和 $Q$ 的概率密度函数。\n> 这里假设两个分布均为连续型概率分布，对于离散型概率分布，只需要将积分替换为对所有离散值遍历求和。\n\n> `KL` 散度满足非负性和不满足对称性。将上式展开可得：\n> $$\\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)$$\n> $$\\text{交叉熵} H(P,Q) = \\mathbb{E}_{\\textrm{x}\\sim p} log Q(x) = - \\int_{-\\infty }^{+\\infty} p(x) logq(x)dx$$\n\n> 其中，$H(P)$ 为熵（`entropy`），$H(P,Q)$ 为交叉熵（`cross entropy`）。\n\n>在信息论中，熵 $H(P)$ 表示对来自 $P$ 的随机遍历进行编码所需的最小字节数，而交叉熵 $H(P,Q)$ 表示使用 $Q$ 的编码对来自 $P$ 的变量进行编码所需的字节数。因此 KL  散度可认为是使用基于 $Q$ 的编码对来自 $P$ 的变量进行编码所需的“额外字节数”；显然，额外字节数非负，当且仅当 $P=Q$ 时额外字节数为 `0`。\n\n## 3.14，结构化概率模型\n\n略\n\n## 参考资料\n\n+ https://zh.m.wikipedia.org/zh-hans/%E6%96%B9%E5%B7%AE#\n+ 《深度学习》\n+ 《机器学习》"
  },
  {
    "path": "1-math_ml_basic/随机梯度下降法的数学基础.md",
    "content": "---\nlayout: post\ntitle: 随机梯度下降法的数学基础\ndate: 2023-01-20 23:00:00\nsummary: 本文从导数开始讲起，讲述了导数、偏导数、方向导数和梯度的定义、意义和数学公式，有助于初学者后续更深入理解随机梯度下降算法的公式。大部分内容来自维基百科和博客文章内容的总结，并加以个人理解。\ncategories: DeepLearning\n---\n\n> 梯度是微积分中的基本概念，也是机器学习解优化问题经常使用的数学工具（梯度下降算法）。因此，有必要从头理解梯度的来源和意义。本文从导数开始讲起，讲述了导数、偏导数、方向导数和梯度的定义、意义和数学公式，有助于初学者后续更深入理解随机梯度下降算法的公式。大部分内容来自维基百科和博客文章内容的总结，并加以个人理解。\n\n## 导数\n\n导数（英语：`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}$\n\n导数是函数的局部性质。不是所有的函数都有导数，一个函数也不一定在所有的点上都有导数。若某函数在某一点导数存在，则称其在这一点可导(可微分)，否则称为不可导(不可微分)。如果函数的自变量和取值都是实数的话，那么函数在某一点的导数就是该函数所代表的曲线在这一点上的切线斜率。\n\n<center>\n<img src=\"../images/bp/curve_slope.png\" width=\"50%\" alt=\"导数曲线\">\n</center>\n\n对于可导的函数 $f$，$x \\mapsto f'(x)$ 也是一个函数，称作 $f$ 的导函数。导数示例如下图所示:\n\n<center>\n<img src=\"../images/bp/Tangent_function_animation.gif\" width=\"50%\" alt=\"函数每个位置处的导数\">\n</center>\n\n导数的一般定义如下:\n\n如果实函数 $f$ 在点 $a$ 的某个领域内有定义，且以下极限（注意这个表达式所定义的函数定义域不含 $a$ ）\n\n$$\n{\\displaystyle \\lim _{x\\to a}{\\frac {f(x)-f(a)}{x-a}}}\n$$\n\n存在，则称 $f$ 于 $a$ 处可导，并称这个极限值为 $f$于$a$ 处的导数，记作 $f'(a)$。\n\n## 常用初等函数的导数公式\n\n![常用初等函数的导数公式](../images/bp/derivative_formula.png)\n\n## 偏导数\n> 偏导数的作用与价值在向量分析和微分几何以及机器学习领域中受到广泛认可。\n\n导数是一元函数的变化率（斜率），导数也是函数，可以理解为函数的变化率与位置的关系。\n\n那么如果是多元函数的变化率问题呢？答案是**偏导数**，定义为**多元函数沿坐标轴的变化率**。\n\n偏导数是多元函数“退化”成一元函数时的导数，这里“退化”的意思是固定其他变量的值，只保留一个变量，依次保留每个变量，则 $N$ 元函数有 $N$ 个偏导数。\n\n如果一个变量对应一个坐标轴，那么偏导数可以理解为函数在每个位置处沿着自变量坐标轴方向上的导数（切线斜率）。\n\n在数学中，偏导数（英语：`partial derivative`）的定义是：一个**多变量**的函数（或称多元函数），**对其中一个变量（导数）微分，而保持其他变量恒定**。函数 $f$ 关于变量 $x$ 的偏导数记为 $f'(x)$ 或 $\\frac{\\partial f}{\\partial x}$。偏导数符号 $\\partial $ 是全导数符号 $d$ 的变体。\n\n假设 $f$ 是一个多元函数。例如：\n\n$$z = f(x, y) = x^2 + xy + y^2$$\n\n我们把变量 $y$ 视为常数，通过对方程求导，我们可以得到**函数 $f$ 关于变量 $x$ 的偏导数**:\n\n$$\\displaystyle {\\frac {\\partial f}{\\partial x}} = 2x + y$$\n\n同理可得，函数 $f$ 关于变量 $y$ 的偏导数:\n\n$$\\frac {\\partial f}{\\partial y} = x + 2y$$\n\n## 方向导数\n\n在前面导数和偏导数的定义中，均是**沿坐标轴正方向讨论函数的变化率**。那么当我们讨论函数沿任意方向的变化率时，也就引出了方向导数的定义，即：**某一点在某一趋近方向上的导数值**。\n\n通俗理解就是：我们不仅要知道函数在坐标轴正方向上的变化率（即偏导数），而且还要设法求得函数在其他特定方向上的变化率（方向导数）。如下图所示，点 $P$ 位置处红色箭头方向的方向导数为黑色切线的斜率。图片来自链接 [Directional Derivative](https://www.geogebra.org/m/Bx8nFMNc)。\n\n<center>\n<img src=\"../images/bp/Directional_Derivative_Visual.png\" width=\"70%\" alt=\"Directional Derivative Visual\">\n</center>\n\n方向导数的定义参考下图，来源-[直观理解梯度，以及偏导数、方向导数和法向量等](https://www.cnblogs.com/shine-lee/p/11715033.html)。\n\n<center>\n<img src=\"../images/bp/Direction_Derivative_Definition.png\" width=\"70%\" alt=\"方向导数计算推导\">\n![方向导数计算推导](../images/bp/Direction_Derivative_Definition.png)\n</center>\n\n## 梯度\n\n在向量微积分中，梯度（英语：`gradient`）是一种关于多元导数的概括。平常的一元（单变量）函数的导数是标量值函数，而多元函数的梯度是**向量值**函数。\n\n就像一元函数的导数表示这个函数图形的切线的斜率，如果多元函数在点 $P$ 上的梯度不是零向量，则它的方向是这个函数在 $P$ 上最大增长的方向、而它的量是在这个方向上的**增长率**。\n\n梯度，写作 $\\nabla f$ 或 grad $f$，二元时为($\\frac{\\partial f(x,y)}{\\partial x}, \\frac{\\partial f(x,y)}{\\partial y}$)。梯度是微积分中的基本概念，也是机器学习解优化问题经常使用的数学工具（梯度下降算法）。借助前面方向导数的推导公式，我们可以得到 $xy$ 平面上一点 $(a,b)$ 处 $\\theta$ 方向上的方向导数和其意义如下图:\n\n![grad_compute](../images/bp/grad_compute.png)\n\n可以从以下两个实例理解**梯度意义**：\n\n1. 假设有一个房间，房间内所有点的温度由一个标量场 $\\phi$ 给出的，即点 $(x,y,z)$ 的温度是 $\\phi(x,y,z)$。假设温度不随时间改变。然后，在房间的每一点，该点的梯度将显示变热最快的方向。梯度的大小将表示在该方向上的温度变化率。\n\n2. 考虑一座高度函数为 $H$ 的山，山上某点 $(x, y)$ 的高度是 $H(x, y)$，点 $(x,y)$ 的梯度是在该点坡度（或者说斜度）最陡的方向。梯度的大小会告诉我们坡度到底有多陡。\n\n总结梯度的几何意义：\n\n- 当前位置的**梯度方向**，为函数在该位置处方向导数最大的方向，也是函数值上升最快的方向，反方向为下降最快的方向；\n- 当前位置的**梯度长度**（模），为最大方向导数的值。\n\n## 总结\n\n- 方向导数是各个方向上的导数。\n- 偏导数连续才有梯度存在。\n- 偏导数构成的向量为梯度。\n- **梯度的方向是方向导数中取到最大值的方向**，梯度的值是方向导数的最大值。\n\n## 参考资料\n\n1. [维基百科-偏导数](https://zh.m.wikipedia.org/zh-hans/%E5%81%8F%E5%AF%BC%E6%95%B0)\n2. [直观理解梯度，以及偏导数、方向导数和法向量等](https://www.cnblogs.com/shine-lee/p/11715033.html)\n3. [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)\n4. [如何理解梯度下降法？](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)\n"
  },
  {
    "path": "2-deep_learning_basic/cnn基础部件-BN层详解.md",
    "content": "- [一，数学基础](#一数学基础)\n  - [1.1，概率密度函数](#11概率密度函数)\n  - [1.2，正态分布](#12正态分布)\n- [二，背景](#二背景)\n  - [2.1，如何理解 Internal Covariate Shift](#21如何理解-internal-covariate-shift)\n  - [2.2，Internal Covariate Shift 带来的问题](#22internal-covariate-shift-带来的问题)\n  - [2.3，减少 Internal Covariate Shift 的一些尝试](#23减少-internal-covariate-shift-的一些尝试)\n- [三，批量归一化（BN）](#三批量归一化bn)\n  - [3.1，BN 的前向计算](#31bn-的前向计算)\n  - [3.2，BN 层如何工作](#32bn-层如何工作)\n  - [3.3，推理时的 BN 层](#33推理时的-bn-层)\n  - [3.4，实验](#34实验)\n  - [3.5，BN 层的作用](#35bn-层的作用)\n- [参考资料](#参考资料)\n\n## 一，数学基础\n\n### 1.1，概率密度函数\n\n随机变量（random variable）是可以随机地取不同值的变量。随机变量可以是离散的或者连续的。简单起见，本文用大写字母 $X$ 表示随机变量，小写字母 $x$ 表示随机变量能够取到的值。例如，$x_1$ 和 $x_2$ 都是随机变量 $X$ 可能的取值。随机变量必须伴随着一个概率分布来指定每个状态的可能性。\n\n概率分布（probability distribution）用来描述随机变量或一簇随机变量在每一个可能取到的状态的可能性大小。我们描述概率分布的方式取决于随机变量是离散的还是连续的。\n\n当我们研究的对象是连续型随机变量时，我们用**概率密度函数**（probability density function, `PDF`）而不是概率质量函数来描述它的概率分布。\n\n> 更多内容请阅读《花书》第三章-概率与信息论，或者我的文章-[深度学习数学基础-概率与信息论](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)。\n\n### 1.2，正态分布\n\n> 当我们不知道数据真实分布时使用正态分布的原因之一是，正态分布拥有最大的熵，我们通过这个假设来施加尽可能少的结构。\n\n实数上最常用的分布就是正态分布(normal distribution)，也称为高斯分布 (Gaussian distribution)。\n\n如果随机变量 $X$ ，服从位置参数为 $\\mu$、尺度参数为 $\\sigma$ 的概率分布，且其概率密度函数为:\n\n$$\nf(x)=\\frac{1}{\\sigma\\sqrt{2 \\pi} } e^{- \\frac{{(x-\\mu)^2}}{2\\sigma^2}} \\tag{1}\n$$\n则这个随机变量就称为正态随机变量，正态随机变量服从的概率分布就称**为正态分布**，记作：\n$$\nX \\sim N(\\mu,\\sigma^2) \\tag{2}\n$$\n如果位置参数 $\\mu = 0$，尺度参数 $\\sigma = 1$ 时，则称为标准正态分布，记作：\n$$\nX \\sim N(0, 1) \\tag{3}\n$$\n此时，概率密度函数公式简化为:\n$$\nf(x)=\\frac{1}{\\sqrt{2 \\pi}} e^{- \\frac{x^2}{2}} \\tag{4}\n$$\n正态分布的数学期望值或期望值 $\\mu$ 等于位置参数，决定了分布的位置；其方差 $\\sigma^2$ 的开平方或标准差 $\\sigma$ 等于尺度参数，决定了分布的幅度。正态分布的概率密度函数曲线呈钟形，常称之为**钟形曲线**，如下图所示:\n\n![正态分布概率密度函数曲线](../images/bn/normal-distribution-curve-1.png)\n\n可视化正态分布，可直接通过 `np.random.normal` 函数生成指定均值和标准差的正态分布随机数，然后基于 `matplotlib + seaborn` 库 `kdeplot`函数绘制概率密度曲线。示例代码如下所示：\n\n```py\nimport seaborn as sns\nx1 = np.random.normal(0, 1, 100)\nx2 = np.random.normal(0, 1.5, 100) \nx3 = np.random.normal(2, 1.5, 100) \n\nplt.figure(dpi = 200)\n\nsns.kdeplot(x1, label=\"μ=0, σ=1\")\nsns.kdeplot(x2, label=\"μ=0, σ=1.5\")\nsns.kdeplot(x3, label=\"μ=2, σ=2.5\")\n\n#显示图例\nplt.legend()\n#添加标题\nplt.title(\"Normal distribution\")\nplt.show()\n```\n\n以上代码直接运行后，输出结果如下图：\n\n![不同参数的正态分布函数曲线](../images/bn/normal_distribution_curve.png)\n\n当然也可以自己实现正态分布的概率密度函数，代码和程序输出结果如下:\n\n```python\nimport numpy as np\nimport matplotlib.pyplot as plt\nplt.figure(dpi = 200)\nplt.style.use('seaborn-darkgrid') # 主题设置\n\ndef nd_func(x, sigma, mu):\n  \t\"\"\"自定义实现正态分布的概率密度函数\n  \t\"\"\"\n    a = - (x-mu)**2 / (2*sigma*sigma)\n    f = np.exp(a) / (sigma * np.sqrt(2*np.pi))\n    return f\n\nif __name__ == '__main__':\n    x = np.linspace(-5, 5)\n    f = nd_fun(x, 1, 0)\n    p1, = plt.plot(x, f)\n\n    f = nd_fun(x, 1.5, 0)\n    p2, = plt.plot(x, f)\n\n    f = nd_fun(x, 1.5, 2)\n    p3, = plt.plot(x, f)\n\n    plt.legend([p1 ,p2, p3], [\"μ=0,σ=1\", \"μ=0,σ=1.5\", \"μ=2,σ=1.5\"])\n    plt.show()\n```\n\n![自己实现的不同参数的正态分布函数曲线](../images/bn/normal_distribution_curve2.png)\n\n## 二，背景\n\n训练深度神经网络的复杂性在于，因为前面的层的参数会发生变化导致每层输入的分布在训练过程中会发生变化。这又导致模型需要需要较低的学习率和非常谨慎的参数初始化策略，从而减慢了训练速度，并且具有饱和非线性的模型训练起来也非常困难。\n\n网络层输入数据分布发生变化的这种现象称为**内部协变量转移**，BN 就是来解决这个问题。\n\n### 2.1，如何理解 Internal Covariate Shift\n\n在深度神经网络训练的过程中，由于网络中参数变化而引起网络中间层数据分布发生变化的这一过程被称在论文中称之为**内部协变量偏移**（Internal Covariate Shift）。\n\n那么，为什么网络中间层数据分布会发生变化呢？\n\n在深度神经网络中，我们可以将每一层视为对输入的信号做了一次变换（暂时不考虑激活，因为激活函数不会改变输入数据的分布）：\n$$\nZ = W \\cdot X + B \\tag{5}\n$$\n其中 $W$ 和 $B$ 是模型学习的参数，这个公式涵盖了全连接层和卷积层。\n\n随着 SGD 算法更新参数，和网络的每一层的输入数据经过公式5的运算后，其 $Z$ 的**分布一直在变化**，因此网络的每一层都需要不断适应新的分布，这一过程就被叫做 Internal Covariate Shift。\n\n而深度神经网络训练的复杂性在于每层的输入受到前面所有层的参数的影响—因此当网络变得更深时，网络参数的微小变化就会被放大。\n\n### 2.2，Internal Covariate Shift 带来的问题\n\n1. 网络层需要不断适应新的分布，**导致网络学习速度的降低**。\n\n2. 网络层输入数据容易陷入到非线性的饱和状态并**减慢网络收敛**，这个影响随着网络深度的增加而放大。\n\n   随着网络层的加深，后面网络输入 $x$ 越来越大，而如果我们又采用 `Sigmoid` 型激活函数，那么每层的输入很容易移动到非线性饱和区域，此时梯度会变得很小甚至接近于 $0$，导致参数的更新速度就会减慢，进而又会放慢网络的收敛速度。\n\n饱和问题和由此产生的梯度消失通常通过使用修正线性单元激活（$ReLU(x)=max(x,0)$），更好的参数初始化方法和小的学习率来解决。然而，如果我们能保证非线性输入的分布在网络训练时保持更稳定，那么优化器将不太可能陷入饱和状态，进而训练也将加速。\n\n### 2.3，减少 Internal Covariate Shift 的一些尝试\n\n1. **白化（Whitening）**: 即输入线性变换为具有零均值和单位方差，并去相关。\n\n   **白化过程由于改变了网络每一层的分布**，因而改变了网络层中本身数据的表达能力。底层网络学习到的参数信息会被白化操作丢失掉，而且白化计算成本也高。\n\n2. **标准化（normalization）**\n\n   Normalization 操作虽然缓解了 `ICS` 问题，让每一层网络的输入数据分布都变得稳定，但却导致了数据表达能力的缺失。\n\n## 三，批量归一化（BN）\n\n### 3.1，BN 的前向计算\n\n论文中给出的 Batch Normalizing Transform 算法计算过程如下图所示。其中输入是一个考虑一个大小为 $m$ 的小批量数据 $\\cal B$。\n\n![Batch Normalizing Transform](../images/bn/bn_algorithm.png)\n\n论文中的公式不太清晰，下面我给出更为清晰的  Batch Normalizing Transform 算法计算过程。\n\n设 $m$ 表示 batch_size 的大小，$n$ 表示 features 数量，即样本特征值数量。在训练过程中，针对每一个 `batch` 数据，`BN` 过程进行的操作是，将这组数据 `normalization`，之后对其进行线性变换，具体算法步骤如下:\n\n$$\\begin{aligned} \n\\mu_B &= \\frac{1}{m}\\sum_1^m x_i \\\\\n\\sigma^2_B &= \\frac{1}{m} \\sum_1^m (x_i-\\mu_B)^2 \\\\\nn_i &= \\frac{x_i-\\mu_B}{\\sqrt{\\sigma^2_B + \\epsilon}} \\\\\nz_i &= \\gamma n_i + \\beta = \\frac{\\gamma}{\\sqrt{\\sigma^2_B + \\epsilon}}x_i + (\\beta - \\frac{\\gamma\\mu_{B}}{\\sqrt{\\sigma^2_B + \\epsilon}})\n\\end{aligned}$$\n\n以上公式乘法都为元素乘，即 `element wise` 的乘法。其中，参数 $\\gamma,\\beta$ 是训练出来的， $\\epsilon$ 是为零防止 $\\sigma_B^2$ 为 $0$ ，加的一个很小的数值，通常为`1e-5`。公式各个符号解释如下:\n\n|     符号     |       数据类型       | 数据形状 |\n| :----------: | :------------------: | :------: |\n|     $X$      |     输入数据矩阵     |  [m, n]  |\n|    $x_i$     | 输入数据第 i 个样本  |  [1, n]  |\n|     $N$      | 经过归一化的数据矩阵 |  [m, n]  |\n|    $n_i$     |  经过归一化的单样本  |  [1, n]  |\n|   $\\mu_B$    |      批数据均值      |  [1, n]  |\n| $\\sigma^2_B$ |      批数据方差      |  [1, n]  |\n|     $m$      |      批样本数量      |   [1]    |\n|   $\\gamma$   |     线性变换参数     |  [1, n]  |\n|   $\\beta$    |     线性变换参数     |  [1, n]  |\n|     $Z$      |   线性变换后的矩阵   |  [1, n]  |\n|    $z_i$     |  线性变换后的单样本  |  [1, n]  |\n|   $\\delta$   |    反向传入的误差    |  [m, n]  |\n\n### 3.2，BN 层如何工作\n\n在论文中，训练一个带 `BN` 层的网络， `BN` 算法步骤如下图所示:\n\n![Training a Batch-Normalized Network](../images/bn/algorithm2.png)\n\n在训练期间，我们一次向网络提供一小批数据。在前向传播过程中，网络的每一层都处理该小批量数据。 `BN` 网络层按如下方式执行前向传播计算：\n\n![Batch Norm 层执行的前向计算](../images/bn/bn_fp.png)\n\n> 图片来源[这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。\n\n注意，图中计算模型推理时 BN 层均值与方差的无偏估计方法，是吴恩达在 Coursera 上的 Deep Learning 课程上提出的方法：对 train 阶段每个 batch 计算的 mean/variance 采用**指数加权平均**来得到 test 阶段 mean/variance 的估计。\n\n在训练期间，它只是计算此 EMA，但不对其执行任何操作。在训练结束时，它只是将该值保存为层状态的一部分，以供在推理阶段使用。\n\n如下图可以展示BN 层的前向传播计算过程数据的 `shape` ，红色框出来的单个样本都指代单个矩阵，即运算都是在单个矩阵运算中计算的。\n\n![Batch Norm 向量的形状](../images/bn/bn_fp_shape.png)\n\n> 图片来源 [这里](https://towardsdatascience.com/batch-norm-explained-visually-how-it-works-and-why-neural-networks-need-it-b18919692739)。\n\nBN 的反向传播过程中，会更新 BN 层中的所有 $\\beta$ 和 $\\gamma$ 参数。\n\n### 3.3，推理时的 BN 层\n\n批量归一化（batch normalization）的“批量”两个字，表示在模型的迭代训练过程中，BN 首先计算小批量（ mini-batch，如 32）的均值和方差。但是，在推理过程中，我们只有一个样本，而不是一个小批量。即在模型训练的时候，均值 $\\mu$、方差 $\\sigma^2$、$\\gamma$、$\\beta$ 是一直在更新的，但是，在推理的时候，这四个值都是固定了的。\n\n虽然 $\\gamma$、$\\beta$  参数可通过模型训练后得到，但是，有一个问题，模型推理时 BN 层的均值和方差应该如何获得呢？\n\n第一种方法是，使用的均值和方差数据是在训练过程中样本值的平均，即：\n\n$$\\begin{align}\nE[x] &= E[\\mu_B] \\nonumber \\\\\nVar[x] &= \\frac{m}{m-1} E[\\sigma^2_B] \\nonumber \\\\\n\\end{align}$$\n\n这种做法会把所有训练批次的 $\\mu$ 和 $\\sigma$ 都保存下来，然后在最后训练完成时（或做测试时）做下平均。\n\n第二种方法是使用类似动量的方法，训练时，加权平均每个批次的值，权值 $\\alpha$ 可以为0.9：\n\n$$\\begin{aligned}\n\\mu_{mov_{i}} &= \\alpha \\cdot \\mu_{mov_{i}} + (1-\\alpha) \\cdot \\mu_i \\nonumber \\\\\n\\sigma_{mov_{i}} &= \\alpha \\cdot \\sigma_{mov_{i}} + (1-\\alpha) \\cdot \\sigma_i \\nonumber \\\\\n\\end{aligned}$$\n\n模型推理或测试时，直接使用模型文件中保存的 $\\mu_{mov_{i}}$ 和 $\\sigma_{mov_{i}}$ 的值即可。\n\n同时，上面 `BN` 的计算公式（算法 1）可以变形为：\n$$\nz_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\n$$\n因为推理时，均值 $\\mu$、方差 $\\sigma^2$、$\\gamma$、$\\beta$ 值固定，因此可以看出推理时 `BN` 本质上是做线性变换，且 $a$、$b$ 都是常数。\n\n### 3.4，实验\n\n`BN` 在 `ImageNet` 分类数据集上实验结果是 `SOTA` 的，如下表所示:\n\n![实验结果表4](../images/bn/Figure_4.png)\n\n### 3.5，BN 层的作用\n\n通过对神经网络中每一层的输入数据进行归一化处理，**使得每一层的输入分布更加稳定**，从而降低了神经网络过拟合的风险。具体而言，BN 层的作用包括：\n\n1. **BN 使得网络中每层输入数据的分布相对稳定，加速模型训练和收敛速度**。\n2. **批标准化可以提高学习率**。在传统的深度网络中，学习率过高可能会导致梯度爆炸或梯度消失，以及陷入差的局部最小值。批标准化有助于解决这些问题。通过标准化整个网络的激活值，它可以防止层参数的微小变化随着数据在深度网络中的传播而放大。例如，这使 sigmoid 非线性更容易保持在它们的非饱和状态，这对训练深度 sigmoid 网络至关重要，但在传统上很难实现。\n3. **BN 允许网络使用饱和非线性激活函数（如 sigmoid，tanh 等）进行训练，其能缓解梯度消失问题**。如果不使用 BN，由于深度神经网络的“累积”效应，使得低层网络的变化效应会累加到高层网络，导致模型训练过程很容易进入到 sigmoid 激活函数的梯度饱和区；而经过 BN 层操作后可以让激活函数的输入数据落在梯度非饱和区，缓解梯度消失的问题。（bn 层一般在激活函数层前面）\n4. BN 层能够**减少过拟合和提高模型的泛化能力**，不再需要 `dropout` 和 `LRN`（Local Response Normalization）层来实现正则化。BN 层将每个特征在当前的 mini-batch 中进行标准化，其中包括均值和方差的估计。由于mini-batch的选择是随机的，所以在每个 mini-batch 中使用的均值和方差的估计也是随机的，这样就引入了一定的噪声。\n5. **减少对参数初始化方法的依赖**。早期的权重是随机初始化的，异常权重值会扭曲梯度，导致网络收敛需要更长的时间，而 `Batch Norm` 有助于抑制这些权重异常值的影响。\n\nSigmoid 导数的**梯度消失区域**如下图所示：\n\n![sigmoid 函数及其导数图像](../images/activation_function/sigmoid_and_gradient_curve2.png)\n\n## 参考资料\n\n1. [Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift](https://arxiv.org/abs/1502.03167)\n2. [维基百科-正态分布](https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E6%80%81%E5%88%86%E5%B8%83)\n3. [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)\n4. [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)\n5. [Batch Normalization原理与实战](https://zhuanlan.zhihu.com/p/34879333)"
  },
  {
    "path": "2-deep_learning_basic/cnn基础部件-卷积层详解.md",
    "content": "---\nlayout: post\ntitle: cnn 基础部件-卷积层详解\ndate: 2022-12-15 22:00:00\nsummary: 卷积神经网络核心网络层是卷积层，其使用了卷积(convolution)这种数学运算，卷积是一种特殊的线性运算。\ncategories: DeepLearning\n---\n\n- [前言](#前言)\n- [一，卷积](#一卷积)\n  - [1.1，卷积运算定义](#11卷积运算定义)\n  - [1.2，卷积的意义](#12卷积的意义)\n  - [1.3，从实例理解卷积](#13从实例理解卷积)\n  - [1.4，图像卷积（二维卷积）](#14图像卷积二维卷积)\n  - [1.5，互相关和卷积](#15互相关和卷积)\n- [二，卷积层](#二卷积层)\n  - [2.1，卷积层定义](#21卷积层定义)\n    - [2.1.1，局部连接](#211局部连接)\n    - [2.1.2，权重共享](#212权重共享)\n  - [2.2，卷积层理解](#22卷积层理解)\n  - [2.3，分组卷积和DW卷积](#23分组卷积和dw卷积)\n  - [2.4，卷积层 api](#24卷积层-api)\n  - [4.5，卷积层代码简单实现](#45卷积层代码简单实现)\n- [三，卷积神经网络](#三卷积神经网络)\n  - [3.1，汇聚层](#31汇聚层)\n  - [3.2.，汇聚层 api](#32汇聚层-api)\n- [四，卷积神经网络结构](#四卷积神经网络结构)\n- [参考资料](#参考资料)\n\n## 前言\n\n在全连接层构成的多层感知机网络中，我们通过将图像数据展平成一维向量来送入模型，这样会忽略了每个图像的**空间结构信息**。理想的策略应该是要利用相近像素之间的相互关联性，将图像数据二维矩阵送给模型中学习。\n\n卷积神经网络(convolutional neural network，`CNN`)正是一类强大的、专为处理图像数据（多维矩阵）而设计的神经网络。在 `Transformer` 应用到 `CV` 领域之前，基于卷积神经网络架构的模型在计算机视觉领域中占主导地位，几乎所有的图像识别、目标检测、语义分割、3D目标检测、视频理解等任务都是以 `CNN` 方法为基础。\n\n卷积神经网络核心网络层是卷积层，其使用了卷积(convolution)这种数学运算，卷积是一种特殊的线性运算。另外，通常来说，卷积神经网络中用到的卷积运算和其他领域(例如工程领域以及纯数学领域)中的定义并不完全一致。\n\n## 一，卷积\n\n在理解卷积层之前，我们首先得理解什么是卷积操作。\n\n卷积与[傅里叶变换](https://zh.wikipedia.org/wiki/傅里叶变换)有着密切的关系。例如两函数的傅里叶变换的乘积等于它们卷积后的傅里叶变换，利用此一性质，能简化傅里叶分析中的许多问题。\n\n> operation 视语境有时译作“操作”，有时译作“运算”，本文不做区分。\n\n### 1.1，卷积运算定义\n\n为了给出卷积的定义， 这里从现实世界会用到函数的例子出发。\n\n假设我们正在用激光传感器追踪一艘宇宙飞船的位置。我们的激光传感器给出 一个单独的输出 $x(t)$，表示宇宙飞船在时刻 $t$ 的位置。$x$ 和  $t$ 都是**实值**的，这意味着我们可以在任意时刻从传感器中读出飞船的位置。\n\n现在假设我们的传感器受到一定程度的噪声干扰。为了得到飞船位置的低噪声估计，我们对得到的测量结果进行平均。显然，时间上越近的测量结果越相关，所 以我们采用一种**加权平均**的方法，对于最近的测量结果赋予更高的权重。我们可以采用一个加权函数 $w(a)$ 来实现，其中 $a$ 表示测量结果距当前时刻的时间间隔。如果我们对任意时刻都采用这种加权平均的操作，就得到了一个新的对于飞船位置的平滑估计函数 $s$:\n\n$$s(t) = \\int x(a)w(t-a )da$$\n\n这种运算就叫做卷积（`convolution`）。更一般的，卷积运算的数学公式定义如下：\n\n$$\n连续定义: \\; h(x)=(f*g)(x) = \\int_{-\\infty}^{\\infty} f(t)g(x-t)dt \\tag{1}\n$$\n\n$$\n离散定义: \\; h(x) = (f*g)(x) = \\sum^{\\infty}_{t=-\\infty} f(t)g(x-t) \\tag{2}\n$$\n\n以上卷积计算公式可以这样理解:\n\n1. 先对函数 $g(t)$ 进行反转（`reverse`），相当于在数轴上把 $g(t)$ 函数从右边褶到左边去，也就是卷积的“卷”的由来。\n2. 然后再把 $g(t)$ 函数向左平移 $x$ 个单位，在这个位置对两个函数的对应点相乘，然后相加，这个过程是卷积的“积”的过程。\n\n### 1.2，卷积的意义\n\n对卷积这个名词，可以这样理解：**所谓两个函数的卷积（$f*g$），本质上就是先将一个函数翻转，然后进行滑动叠加**。在连续情况下，叠加指的是对两个函数的乘积求积分，在离散情况下就是加权求和，为简单起见就统一称为叠加。\n\n因此，卷积运算整体来看就是这么一个过程:\n\n翻转—>滑动—>叠加—>滑动—>叠加—>滑动—>叠加.....\n\n多次滑动得到的一系列叠加值，构成了卷积函数。\n\n> 这里多次滑动过程对应的是 $t$ 的变化过程。\n\n那么，**卷积的意义是什么呢**？可以从卷积的典型应用场景-图像处理来理解：\n\n1. 为什么要进行“卷”？进行“卷”（翻转）的目的其实是施加一种约束，它指定了在“积”的时候以什么为参照。在空间分析的场景，它指定了在哪个位置的周边进行累积处理。\n2. 在图像处理的中，卷积处理的结果，其实就是把每个像素周边的，甚至是整个图像的像素都考虑进来，对当前像素进行某种加权处理。因此，“积”是全局概念，或者说是一种“混合”，把两个函数进行时间（信号分析）或空间（图像处理）上进行混合。\n\n> 卷积意义的理解来自[知乎问答](https://www.zhihu.com/question/22298352)，有所删减和优化。\n\n### 1.3，从实例理解卷积\n\n一维卷积的实例有 “丢骰子” 等经典实例，这里不做展开描述，本文从二维卷积用于图像处理的实例来理解。\n\n一般，数字图像可以表示为如下所示矩阵：\n> 本图片摘自[知乎用户马同学的文章](https://www.zhihu.com/question/22298352)。\n\n![图像矩阵](../images/conv/image1.png)\n\n而卷积核 $g$ 也可以用一个矩阵来表示，如:\n\n$$\ng = \\begin{bmatrix} \n&b_{-1,-1} &b_{-1,0} &b_{-1,1} \\\\ \n&b_{0,-1} &b_{0,0} &b_{0,1} \\\\ \n&b_{1,-1} &b_{1,0} &b_{1,1}\n\\end{bmatrix}\n$$\n\n按照卷积公式的定义，则目标图片的第 $(u, v)$ 个像素的二维卷积值为：\n\n$$\n(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}\n$$\n展开来分析二维卷积计算过程就是，首先得到原始图像矩阵中 $(u, v)$ 处的矩阵：\n\n$$\nf=\\begin{bmatrix} \n&a_{u-1,v-1} &a_{u-1,v} &a_{u-1,v+1}\\\\ \n&a_{u,v-1} &a_{u,v} &a_{u,v+1} \\\\ \n&a_{u+1,v-1} &a_{u+1,v} &a_{u+1,v+1}\n\\end{bmatrix}\n$$\n\n然后将图像处理矩阵**翻转**（两种方法，结果等效），如先沿 $x$ 轴翻转，再沿 $y$ 轴翻转（**相当于将矩阵 $g$ 旋转 180 度**）：\n\n$$\n\\begin{aligned}\ng &= \\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}\n=> \\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} \\\\\n&= \\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^{'}\n\\end{aligned}\n$$\n\n最后，计算卷积时，就可以用 $f$ 和 $g′$ 的内积：\n\n$$\n\\begin{aligned}\nf * 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} \\\\ \n&+ a_{u,v-1} \\times b_{0,1} + a_{u,v} \\times b_{0,0} + a_{u,v+1} \\times b_{0,-1} \\\\ \n&+ 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}\n\\end{aligned}\n$$\n\n计算过程可视化如下动图所示，注意动图给出的是 $g$ 不是 $g'$。\n\n![二维卷积计算过程](../images/conv/conv_visual.gif)\n\n以上公式有一个特点，做乘法的两个对应变量 $a, b$ 的下标之和都是 $(u,v)$，其目的是对这种加权求和进行一种约束，这也是要将矩阵 $g$ 进行翻转的原因。上述计算比较麻烦，实际计算的时候，都是用翻转以后的矩阵，直接求**矩阵内积**就可以了。\n\n### 1.4，图像卷积（二维卷积）\n\n在机器学习和图像处理领域，卷积的主要功能是**在一个图像(或某种特征) 上滑动一个卷积核(即滤波器)，通过卷积操作得到一组新的特征**。一幅图像在经过卷积操作后得到结果称为特征映射(`Feature Map`)。如果把图像矩阵简写为 $I$，把卷积核 `Kernal` 简写为 $K$，则目标图片的第 $(i,j)$ 个像素的卷积值为：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(m,n)K(i-m,j-n) \\tag{3}\n$$\n\n可以看出，这和一维情况下的卷积公式 2 是一致的。因为卷积的可交换性，我们也可以把公式 3 等价地写作：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(i-m,j-n)K(m,n) \\tag{4}\n$$\n\n通常，下面的公式在机器学习库中实现更为简单，因为 $m$ 和 $n$ 的有效取值范围相对较小。\n\n卷积运算可交换性的出现是因为我们将核相对输入进行了翻转（`flip`），从 $m$ 增 大的角度来看，输入的索引在增大，但是卷积核的索引在减小。我们将**卷积核翻转的唯一目 的是实现可交换性**。尽管可交换性在证明时很有用，但在神经网络的应用中却不是一个重要的性质。相反，许多神经网络库会实现一个**互相关函数**（`corresponding function`），它与卷积相同但没有翻转核：\n\n$$\nh(i,j) = (I*K)(i,j)=\\sum_m \\sum_n I(i+m,j+n)K(m,n) \\tag{5}\n$$\n\n互相关函数的运算，是两个序列滑动相乘，两个序列都不翻转。卷积运算也是滑动相乘，但是其中一个序列需要先翻转，再相乘。\n\n### 1.5，互相关和卷积\n\n互相关和卷积运算的关系，可以通过下述公式理解：\n\n$$\n\\begin{aligned}Y \n&= W\\otimes X \\\\\n&= \\text{rot180}(W) * X \\\\\n\\end{aligned}\n$$\n\n其中 $\\otimes$ 表示互相关运算，$*$ 表示卷积运算，$\\text{rot180(⋅)}$ 表示旋转 `180` 度，$Y$ 为输出矩阵。从上式可以看出，互相关和卷积的区别仅仅在于卷积核是否进行翻转。因此互相关也可以称为不翻转卷积.\n> 离散卷积可以看作矩阵的乘法，然而，这个矩阵的一些元素被限制为必须和另外一些元素相等。\n\n在神经网络中使用卷积是为了进行特征抽取，**卷积核是否进行翻转和其特征抽取的能力无关**（特别是当卷积核是可学习的参数时），**因此卷积和互相关在能力上是等价的**。事实上，很多深度学习工具中卷积操作其实都是互相关操作，用来**减少一些不必要的操作或开销（不反转 Kernal）**。\n\n总的来说，\n1. 我们实现的卷积操作不是原始数学含义的卷积，而是工程上的卷积，但一般也简称为卷积。\n2. 在实现卷积操作时，并不会反转卷积核。\n\n## 二，卷积层\n\n> 在传统图像处理中，**线性空间滤波**的原理实质上是指指图像 $f$ 与滤波器核 $w$ 进行乘积之和（**卷积**）运算。核是一个矩阵，其大小定义了运算的邻域，其系数决定了该滤波器（也称模板、窗口滤波器）的性质，并通过设计不同核系数（卷积核）来实现低通滤波（平滑）和高通滤波（锐化）功能，因此我们可以认为卷积是利用某些设计好的参数组合（卷积核）去提取图像空域上相邻的信息。\n\n### 2.1，卷积层定义\n\n在全连接前馈神经网络中，如果第 $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}$ 都很大时，权重矩阵的参数就会非常多，训练的效率也会非常低。\n\n如果采用卷积来代替全连接，第 $l$ 层的净输入 $z^{(l)}$ 为第 $l-1$ 层激活值 $a^{(l−1)}$ 和滤波器 $w^{(l)}\\in \\mathbb{R}^K$ 的卷积，即\n$$\nz^{(l)} = w^{(l)}\\otimes a^{(l−1)} + b^{(l)}\n$$\n其中 $b^{(l)}\\in \\mathbb{R}$ 为可学习的偏置。\n\n> 上述卷积层公式也可以写成这样的形式：$Z = W*A+b$\n\n根据卷积层的定义，卷积层有两个很重要的性质: 局部连接和权重共享。\n\n#### 2.1.1，局部连接\n\n**局部连接**：在卷积层(假设是第 $l$ 层)中的每一个神经元都只和下一层(第 $l + 1$ 层)中某个局部窗口内的神经元相连，构成一个局部连接网络。其实可能表达为**稀疏交互**更直观点，传统的网络层是全连接的，使用矩阵乘法来建立输入与输出的连接关系。矩阵的每个参数都是独立的，它描述了每个输入单元与输出单元的交互。这意味着每个输出单元与所有的输入单元都产生关联。而卷积层通过使用**卷积核矩阵**来实现稀疏交互（也称作稀疏连接，或者稀疏权重），每个输出单元仅仅与特定的输入神经元（其实是指定通道的 `feature map`）产生关联。\n\n下图显示了全连接层和卷积层的每个输入单元影响的输出单元比较:\n\n![全连接层和卷积层对比](../images/conv/ful_conv2.png)\n\n- 对于传统全连接层，每个输入单元影响了所有的输出单元。\n- 对于卷积层，每个输入单元只影响了3个输出单元（核尺寸为3时）。\n\n#### 2.1.2，权重共享\n\n**权重共享**：卷积层中，同一个核会在输入的不同区域做卷积运算。全连接层和卷积层的权重参数比较如下图:\n\n![全连接层和卷积层对比](../images/conv/ful_conv3.png)\n\n- 对于传统全连接层: $x_3\\to s_3$ 的权重 $w_{3,3}$ 只使用了一次 。\n- 对于卷积层： $x_3\\to s_3$ 的权重 $w_{3,3}$ 被共享到  $x_i \\to s_i, i = 1,2,4,5$。\n\n全连接层和卷积层的可视化对比如下图所示:\n\n![全连接层和卷积层对比](../images/conv/ful_conv_compare.png)\n\n总结：一个滤波器（3维卷积核）只捕捉输入数据中的一种特定的局部特征。为了提高卷积网络的表示能力，可以在每一层使用多个不同的特征映射，即增加滤波器的数量，以更好地提取图像的特征。\n\n### 2.2，卷积层理解\n\n前面章节内容中，卷积的输出形状只取决于输入形状和卷积核的形状。而神经网络中的卷积层，在卷积的标准定义基础上，还引入了卷积核的滑动步长和零填充来增加卷积的多样性，从而可以更灵活地进行特征抽取。\n\n- 步长(Stride)：指卷积核每次滑动的距离\n- 零填充(Zero Padding)：在输入图像的边界填充元素(通常填充元素是0)\n\n卷积层定义：每个卷积核（`Kernel`）在输入矩阵上滑动，并通过下述过程实现卷积计算:\n\n1. 在来自卷积核的元素和输入特征图子矩阵的元素之间进行乘法以获得输出感受野。 \n2. 然后将相乘的值与添加的偏差相加以获得输出矩阵中的值。 \n\n通道数为 1 的输入特征图应用卷积层的数值计算过程可视化如下图 1 所示：\n\n![Example of Convolutional layer](../images/conv/Example-of-Convolutional-layer.png)\n\n> 图片来源论文 [Improvement of Damage Segmentation Based on Pixel-Level Data Balance Using VGG-Unet](https://www.researchgate.net/figure/Example-of-Convolutional-layer_fig2_348296106)。\n\n注意，卷积层的输出 `Feature map` 的大小取决于输入的大小、Pad 数、卷积核大小和步长。在 `Pytorch` 框架中，图片（`feature map`）经卷积 `Conv2D` 后**输出大小计算公式**如下：$\\left \\lfloor N = \\frac{W-F+2P}{S}+1 \\right \\rfloor$。\n\n其中 $\\lfloor \\rfloor$ 是向下取整符号，用于结果不是整数时进行向下取整（`Pytorch` 的 `Conv2d` 卷积函数的默认参数 `ceil_mode = False`，即默认向下取整, `dilation = 1`），其他符号解释如下：\n\n+ 输入图片大小 `W×W`（默认输入尺寸为正方形）\n+ `Filter` 大小 `F×F`\n+ 步长 `S`\n+ padding的像素数 `P`\n+ 输出特征图大小 `N×N`\n\n上图1侧重于解释数值计算过程，而下图2则侧重于解释卷积层的五个核心概念的关系：\n\n- 输入 Input Channel\n- 卷积核组 WeightsBias\n- 过滤器 Filter\n- 卷积核 kernal\n- 输出 Feature Map\n\n![三通道经过两组过滤器的卷积过程](../images/conv/conv3d.png)\n\n上图是三通道经过两组过滤器的卷积过程，在这个例子中，输入是三维数据 $3\\times 32 \\times32$，经过权重参数尺寸为 $2\\times 3\\times 5\\times 5$ 的卷积层后，输出为三维 $2\\times 28\\times 28$，维数并没有变化，只是每一维内部的尺寸有了变化，一般都是要向更小的尺寸变化，以便于简化计算。\n\n假设三维卷积核（也叫滤波器）尺寸为 $(c_{in}, k, k)$，一共有 $c_{out}$ 个滤波器，即卷积层参数尺寸为 $(c_{out}, c_{in}, k, k)$ ，则标准卷积层计算有以下**特点**：\n\n1. 输出的 `feature map` 的数量等于滤波器数量 $c_{out}$，即卷积层参数值确定后，feature map 的数量也确定，而不是根据前向计算自动计算出来；\n2. 对于每个输出，都有一个对应的过滤器 Filter，图例中 Feature Map-1 对应 Filter-1；\n3. **每个 Filter 内都有一个或多个卷积核 Kernal，对应每个输入通道(Input Channel)**，图例为 3，对应输入的红绿蓝三个通道；\n4. 每个 Filter 只有一个 Bias 值，图例中 Filter-1 对应 b1；\n5. 卷积核 Kernal 的大小一般是奇数如：$1\\times 1$，$3\\times 3$。\n\n**注意**，以上内容都描述的是**标准卷积**，随着技术的发展，后续陆续提出了分组卷积、深度可分离卷积、空洞卷积等。详情可参考我之前的文章-[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)。\n\n### 2.3，分组卷积和DW卷积\n\n和标准卷积每个 Filter 内都有一个或多个卷积核 Kernal，对应每个输入通道(Input Channel)的特性不同，分组卷积和 DW 卷积的特点如下：\n- 分组卷积：**分组卷积是将输入通道分成若干组**，**每组的滤波器只与其同组的输入 feature map 进行卷积**，最终将每组的输出通道拼接在一起得到最终输出。\n- DW 卷积：每个 Filter 内只有一个卷积核 Kernal，对应每个输入通道(Input Channel)，即对于每个输入通道分别使用一个固定大小的卷积核进行卷积操作。\n\n分组卷积的极致是分组数数等于输入通道数，这其实就是 `DW` 卷积，可视化如下：\n\n![DW卷积](../images/conv/dw_conv.png)\n\n另外，对于 `pytorch` 的卷积层 api 是同时支持普通卷积、分组卷积/DW 卷积的。但值得注意的是，对于分组卷积，卷积层的输出通道数必须是分组数的整数倍，否则代码会报错！\n\n```python\nimport torch\ninput = torch.randn([20, 10, 224, 224]) # input_channels = 10\nconv3x3 = torch.nn.Conv2d(in_channels = 10, output_channels = 5, kernel_size=3, groups=5)\noutput = conv3x3(input)\nprint(conv3x3.weight.shape)\nprint(output.shape)\n```\n\n如果将 `groups=5` 改为 `groups=6`或者将 `output_channels  = 5` 改为 `6`，则会报错：\n```bash\nValueError: in_channels must be divisible by groups\nValueError: out_channels must be divisible by groups\n```\n### 2.4，卷积层 api\n\n注意，`2D` 卷积的卷积核权重是一个 `4D` 张量，包含输出通道，输入通道，高，宽。对于 `Pytorch/Caffe` 深度学习框架，其输入输出数据的尺寸都是 （`(N, C, H, W)`），卷积核权重 `shape` 如下：\n- 常规卷积的卷积核权重 `shape`:（`C_out, C_in, kernel_height, kernel_width`）\n- 分组卷积的卷积核权重 `shape`:（`C_out, C_in/g, kernel_height, kernel_width`）\n- `DW` 卷积的卷积核权重`shape`:（`C_in, 1, kernel_height, kernel_width`）\n\n`Pytorch` 框架中对应的 2D 卷积层 api 如下：\n\n```python\n# 对应常规卷积的卷积核权重 shape 都为（out_channels, in_channels, kernel_height, kernel_width）\nclass torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)\n```\n\n**主要参数解释：**\n\n+ `in_channels`(`int`) – 输入信号的通道。\n+ `out_channels`(`int`) – 卷积产生的通道。\n+ `kerner_size`(`int or tuple`) - 卷积核的尺寸。\n+ `stride`(`int or tuple`, `optional`) - 卷积步长，默认值为 `1` 。\n+ `padding`(`int or tuple`, `optional`) - 输入的每一条边补充 `0` 的层数，默认不填充。\n+ `dilation`(`int or tuple`, `optional`) – 卷积核元素之间的间距，默认取值 `1` 。\n+ `groups`(`int`, `optional`) – 从输入通道到输出通道的阻塞连接数。\n+ `bias`(`bool`, `optional`) - 如果 `bias=True`，添加偏置。\n\n**示例代码：**\n\n```python\n###### Pytorch卷积层输出大小验证\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# With square kernels and equal stride\n# output_shape: height = (50-3)/2+1 = 24.5，卷积向下取整，所以 height=24.\nm = nn.Conv2d(16, 33, 3, stride=2)\n# # non-square kernels and unequal stride and with padding\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))  # 输出shape: torch.Size([20, 33, 28, 100])\n# # non-square kernels and unequal stride and with padding and dilation\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))  # 输出shape: torch.Size([20, 33, 26, 100])\ninput = autograd.Variable(torch.randn(20, 16, 50, 100))\noutput = m(input)\n\nprint(output.shape)  # 输出shape: torch.Size([20, 16, 24, 49])\n```\n\n### 4.5，卷积层代码简单实现\n\n**卷积层参数**和全连接参数是类似的，都是权重 `weights` 和偏移向量 `bias` 的组合，但是区别在于卷积层的权重矩阵是四维的（`conv2d`），而全连接层的权重二维的，但偏移向量都是列向量。\n\n基于 `numpy` 库，没有经过优化版本的卷积层代码实现如下：\n\n```python\nfor bs in range(batch_size):\n  for oc in range(output_channels):\n    for oh in range(output_height):\n      for ow in range(output_weight):\n        # input 三维矩阵 kernel 三维矩阵相乘, 默认 array1*array2 就是对应元素的乘积\n        output[bs, oc, oh, ow] = np.sum(input[bs, :, oh: oh+kernel_size, ow: ow+kernel_size] * weights[oc, :, :, :]) + bias[oc]\nreturn output\n```\n\n如果不用 `numpy` 函数，for 循环的次数会更多，实现方式如下：\n\n```python\nstride = 1\nkernel_size = 3\nfor bs in range(batch_size):\n    for oc in range(output_channels):\n        output[bs, oc, oh, ow] += bias[oc]\n        for ic in range(input_channels):\n            for oh in range(height):\n                for ow in range(width):\n                    for kh in range(kernel_size):\n                        for kw in range(kernel_size):\n                            output[bs, oc, oh, ow] += input[bs, ic, oh+kh, ow+kw] * weights[oc, ic, kh, kw]\n```\n\n\n## 三，卷积神经网络\n\n卷积神经网络一般由卷积层、汇聚层和全连接层构成。\n\n### 3.1，汇聚层\n\n通常当我们处理图像时，我们希望逐渐降低隐藏表示的空间分辨率、聚集信息，这样随着我们在神经网络中层叠的上升，每个神经元对其敏感的感受野（输入）就越大。\n\n汇聚层(Pooling Layer)也叫子采样层(Subsampling Layer)，其作用不仅是进降低卷积层对位置的敏感性，同时降低对空间降采样表示的敏感性。\n\n与卷积层类似，汇聚层运算符由一个固定形状的窗口组成，该窗口根据其步幅大小在输入的所有区域上滑动，为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。然而，不同于卷积层中的输入与卷积核之间的互相关计算，**汇聚层不包含参数**。相反，池运算是确定性的，我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为**最大汇聚层**(maximum pooling)和**平均汇聚层**(average pooling)。通道数为 1 的输入特征图应用最大汇聚层计算过程的可视化如下所示:\n\n![最大汇聚层计算过程的可视化](../images/conv/maxpool_cal_visual.png)\n\n在这两种情况下，与互相关运算符一样，汇聚窗口从输入张量的左上⻆开始，从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置，它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。\n\n比如最大汇聚层，其计算输入中区域的最大值。\n\n![最大汇聚层计算结果](../images/conv/pool_cal2.png)\n\n值得注意的是，与卷积层一样，汇聚层也可以通过改变填充和步幅以获得所需的输出形状。\n\n### 3.2.，汇聚层 api\n\n`Pytorch` 框架中对应的聚合层 api 如下：\n\n```python\nclass torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)\n```\n\n**主要参数解释**：\n\n+ `kernel_size`(`int or tuple`)：`max pooling` 的窗口大小。\n+ `stride`(`int or tuple`, `optional)：`max pooling` 的窗口移动的步长。默认值是 `kernel_size`。\n+ `padding`(`int or tuple`, `optional`)：**默认值为 `0`，即不填充像素**。输入的每一条边补充 `0` 的层数。\n+ `dilation`：滑动窗中各元素之间的距离。\n+ `ceil_mode`：默认值为 `False`，即上述公式默认向下取整，如果设为 `True`，计算输出信号大小的时候，公式会使用向上取整。\n\n> `Pytorch` 中池化层默认`ceil mode = false`，而 `Caffe` 只实现了 `ceil mode= true` 的计算方式。\n\n**示例代码：**\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# 大小为3，步幅为2的正方形窗口池\nm = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n# pool of non-square window\ninput = autograd.Variable(torch.randn(20, 16, 50, 32))\noutput = m(input)\nprint(output.shape)  # torch.Size([20, 16, 25, 16])\n```\n\n## 四，卷积神经网络结构\n\n一个典型的卷积网络结构是由卷积层、汇聚层、全连接层交叉堆叠而成。如下图所示：\n\n![典型的卷积网络整体结构](../images/conv/cnn_structure.png)\n\n一个简单的 `CNN` 网络连接图如下所示。\n> 经典 `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)。\n\n![一个简单的cnn网络结构图](../images/conv/cnn_demo1.jpeg)\n\n目前，卷积网络的整体结构趋向于使用更小的卷积核(比如 $1 \\times 1$ 和 $3 \\times 3$) 以及更深的结构(比如层数大于 50)。另外，由于卷积层的操作性越来越灵活（同样可完成减少特征图分辨率），汇聚层的作用越来越小，因此目前的卷积神经网络逐渐趋向于全卷积网络。\n\n另外，可通过这个[网站](https://poloclub.github.io/cnn-explainer/#article-convolution)可视化 `cnn` 的全部过程。\n\n![cnn_explainer](../images/conv/cnn_explainer.png)\n\n## 参考资料\n\n1. [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)\n2. [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)\n3. 《神经网络与深度学习》-第5章\n4. 《动手学深度学习》-第6章\n5. https://www.zhihu.com/question/22298352\n6. [卷积神经网络](https://www.huaxiaozhuan.com/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/chapters/5_CNN.html)\n"
  },
  {
    "path": "2-deep_learning_basic/cnn基础部件-激活函数详解.md",
    "content": "---\nlayout: post\ntitle: cnn 基础部件-激活函数详解\ndate: 2022-12-05 22:00:00\nsummary: 本文分析了激活函数对于神经网络的必要性，同时讲解了几种常见的激活函数的原理，并给出相关公式、代码和示例图。\ncategories: DeepLearning\n---\n\n- [一 激活函数概述](#一-激活函数概述)\n  - [1.1 前言](#11-前言)\n  - [1.2 激活函数定义](#12-激活函数定义)\n  - [1.3 激活函数性质](#13-激活函数性质)\n- [二 Sigmoid 型函数（挤压型激活函数）](#二-sigmoid-型函数挤压型激活函数)\n  - [2.1 Logistic(sigmoid)函数](#21-logisticsigmoid函数)\n  - [2.2 Tanh 函数](#22-tanh-函数)\n- [三 ReLU 函数及其变体（半线性激活函数）](#三-relu-函数及其变体半线性激活函数)\n  - [3.1 ReLU 函数](#31-relu-函数)\n  - [3.2，Leaky ReLU/PReLU/ELU/Softplus 函数](#32leaky-relupreluelusoftplus-函数)\n- [四 Swish 函数](#四-swish-函数)\n- [五 激活函数总结](#五-激活函数总结)\n- [参考资料](#参考资料)\n\n> 本文分析了激活函数对于神经网络的必要性，同时讲解了几种常见的激活函数的原理，并给出相关公式、代码和示例图。\n\n## 一 激活函数概述\n\n### 1.1 前言\n\n人工神经元(Artificial Neuron)，简称神经元(Neuron)，是构成神经网络的基本单元，其主要是模拟生物神经元的结构和特性，接收一组输入信号并产生输出。生物神经元与人工神经元的对比图如下所示。\n\n<div align=\"center\">\n<img src=\"../images/activation_function/neuron.png\" width=\"60%\" alt=\"neuron\">\n</div>\n\n从机器学习的角度来看，神经网络其实就是一个**非线性模型**，其基本组成单元为具有非线性激活函数的神经元，通过大量神经元之间的连接，使得多层神经网络成为一种高度非线性的模型。**神经元之间的连接权重就是需要学习的参数**，其可以在机器学习的框架下通过**梯度下降方法**来进行学习。\n> 深度学习一般指的是深度神经网络模型，泛指网络层数在三层或者三层以上的神经网络结构。\n\n### 1.2 激活函数定义\n\n激活函数（也称“非线性映射函数”），是深度卷积神经网络模型中必不可少的网络层。\n\n假设一个神经元接收 $D$ 个输入 $x_1, x_2,⋯, x_D$，令向量 $x = [x_1;x_2;⋯;x_𝐷]$ 来表示这组输入，并用净输入(Net Input) $z \\in \\mathbb{R}$ 表示一个神经元所获得的输入信号 $x$ 的加权和:\n\n$$\nz = \\sum_{d=1}^{D} w_{d}x_{d} + b = w^\\top x + b\n$$\n\n其中 $w = [w_1;w_2;⋯;w_𝐷]\\in \\mathbb{R}^D$ 是 $D$ 维的权重矩阵，$b \\in \\mathbb{R}$ 是偏置向量。\n\n以上公式其实就是**带有偏置项的线性变换**（类似于放射变换），本质上还是属于线形模型。为了转换成非线性模型，我们在净输入 $z$ 后添加一个**非线性函数** $f$（即激活函数）。\n\n$$a = f(z)$$\n\n由此，典型的神经元结构如下所示:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/typical_neuron_architecture.png\" width=\"60%\" alt=\"典型的神经元架构\">\n</div>\n\n### 1.3 激活函数性质\n\n为了增强网络的表示能力和学习能力，激活函数需要具备以下几点性质:\n1. **连续并可导(允许少数点上不可导)的非线性函数**。可导的激活函数 可以直接利用数值优化的方法来学习网络参数。\n2. 激活函数及其导函数要**尽可能的简单**，有利于提高网络计算效率。\n3. 激活函数的导函数的**值域要在一个合适的区间内**，不能太大也不能太小，否则会影响训练的效率和稳定性.\n\n## 二 Sigmoid 型函数（挤压型激活函数）\n\nSigmoid 型函数是指一类 S 型曲线函数，为两端饱和函数。常用的 Sigmoid 型函数有 Logistic 函数和 Tanh 函数。\n\n> 相关数学知识: 对于函数 $f(x)$，若 $x \\to −\\infty$ 时，其导数 ${f}'\\to 0$，则称其为左饱和。若 $x \\to +\\infty$ 时，其导数 ${f}'\\to 0$，则称其为右饱和。当同时满足左、右饱和时，就称为两端饱和。\n\n### 2.1 Logistic(sigmoid)函数\n\n对于一个定义域在 $\\mathbb{R}$ 中的输入，`sigmoid` 函数将输入变换为区间 `(0, 1)` 上的输出。因此，sigmoid 通常称为**挤压函数**(squashing function): 它将范围 (-inf, inf) 中的任意输入压缩到区间 (0, 1) 中的某个值:\n\n$$\n\\sigma(x) = \\frac{1}{1 + exp(-x)}\n$$\n\nsigmoid 函数常记作 $\\sigma(x)$。它的导数公式如下所示:\n\n$$\n\\frac{\\mathrm{d} }{\\mathrm{d} x}\\text{sigmoid}(x) = \\frac{exp(-x)}{(1+exp(-x))^2} = \\text{sigmoid}(x)(1 - \\text{sigmoid}(x))\n$$\n\nsigmoid 函数及其导数曲线如下所示:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/sigmoid_and_gradient_curve2.png\" width=\"70%\" alt=\"sigmoid 函数及其导数图像\">\n</div>\n\n可以看出，sigmoid 函数连续，光滑、严格单调，以 (0,0.5) 中心对称，是一个非常良好的阈值函数。\n\n当输入为 0 时，sigmoid 函数的导数达到最大值 0.25; 而输入在任一方向上越远离 0 点时，导数越接近 `0`，即**当sigmoid 函数的输入很大或是很小时，它的梯度都会消失**。\n\n目前 `sigmoid` 函数在隐藏层中已经较少使用，原因是 `sigmoid` 的软饱和性，使得深度神经网络在过去的二三十年里一直难以有效的训练，如今其被更简单、更容易训练的 `ReLU` 等激活函数所替代。\n\n当我们想要输出二分类或多分类、多标签问题的概率时，`sigmoid` **可用作模型最后一层的激活函数**。下表总结了常见问题类型的最后一层激活和损失函数。\n\n|问题类型|最后一层激活 |损失函数|\n|-------|-----------|-------|\n|二分类问题（binary）| `sigmoid` | `sigmoid + nn.BCELoss()` 模型最后一层需要经过 torch.sigmoid 函数 |\n|多分类、单标签问题（Multiclass）|`softmax`|`nn.CrossEntropyLoss()`: 无需手动做 `softmax`|\n|多分类、多标签问题（Multilabel）|`sigmoid`|`sigmoid + nn.BCELoss()`: 模型最后一层需要经过 `sigmoid` 函数|\n\n> `nn.BCEWithLogitsLoss()` 函数等效于 `sigmoid + nn.BCELoss`。\n\n### 2.2 Tanh 函数\n\n`Tanh`（双曲正切）函数也是一种 Sigmoid 型函数，可以看作放大并平移的 `Sigmoid` 函数，公式如下所示：\n\n$$\n\\text{tanh}(x) = 2\\sigma(2x) - 1 = \\frac{2}{1 + e^{-2x}} - 1\n$$\n\n利用基本导数公式，可得 Tanh 函数的导数公式（推导过程省略）:\n\n$$\n\\frac{\\mathrm{d} }{\\mathrm{d} x} \\text{tanh}(x) = 1 - \\text{tanh}^{2}(x)\n$$\n\n**Logistic 和 Tanh 两种激活函数的实现及可视化代码**（复制可直接运行）如下所示:\n\n```python\n# example plot for the sigmoid activation function\nimport numpy as np\nfrom matplotlib import pyplot\nimport matplotlib.pyplot as plt\n\n# sigmoid activation function\ndef sigmoid(x):\n    \"\"\"1.0 / (1.0 + exp(-x))\n    \"\"\"\n    return 1.0 / (1.0 + np.exp(-x))\n\ndef tanh(x):\n    \"\"\"2 * sigmoid(2*x) - 1\n    (e^x – e^-x) / (e^x + e^-x)\n    \"\"\"\n    # return (exp(x) - exp(-x)) / (exp(x) + exp(-x))\n    return 2 * sigmoid(2*x) - 1\n\ndef relu(x):\n    return max(0.0, x)\n\ndef gradient_relu(x):\n    \"\"\"1 * (x > 0)\"\"\"\n    if x < 0.0:\n        return 0\n    else:\n        return 1\n\ndef gradient_sigmoid(x):\n    \"\"\"sigmoid(x)(1−sigmoid(x))\n    \"\"\"\n    a = sigmoid(x)\n    b = 1 - a\n    return a*b\n\ndef gradient_tanh(x):\n    return 1 - tanh(x)**2\n\n# 1, define input data\ninputs = [x for x in range(-6, 7)]\n\n# 2, calculate outputs\noutputs = [sigmoid(x) for x in inputs]\noutputs2 = [tanh(x) for x in inputs]\n\n# 3, plot sigmoid and tanh function curve\nplt.figure(dpi=100) # dpi 设置\nplt.style.use('ggplot') # 主题设置\n\nplt.plot(inputs, outputs, label='sigmoid')\nplt.plot(inputs, outputs2, label='tanh')\n\nplt.xlabel(\"x\") # 设置 x 轴标签\nplt.ylabel(\"y\")\nplt.title('sigmoid and tanh') # 折线图标题\nplt.legend()\nplt.show()\n```\n\n程序运行后得到的 Sigmoid 和 Tanh 函数曲线如下图所示:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/sigmoid_tanh_curve.png\" width=\"70%\" alt=\"Logistic函数和Tanh函数\">\n</div>\n\n以上代码的基础上，改下 plt.plot 函数的输入数据，同样可得到 Tanh 函数及其导数曲线图:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/tanh_and_gradient_curve.png\" width=\"70%\" alt=\"Tanh函数及其导数\">\n</div>\n\n可以看出 `Sigmoid` 和 `Tanh` 函数在输入很大或是很小的时候，**输出都几乎平滑且梯度很小趋近于 0**，不利于权重更新；不同的是 `Tanh` 函数的输出区间是在 `(-1,1)` 之间，而且整个函数是以 0 为中心的，即他本身是零均值的，也就是说，在前向传播过程中，输入数据的均值并不会发生改变，这就使他在很多应用中效果能比 `Sigmoid` 优异一些。\n\n**`Tanh` 函数优缺点总结**：\n\n- 具有 Sigmoid 的所有优点。\n- `exp` 指数计算代价大。梯度消失问题仍然存在。\n\n`Tanh` 函数及其导数曲线如下所示:\n\nTanh 和 Logistic 函数的导数很类似，都有以下特点:\n- 当输入接近 0 时，导数接近最大值 1。\n- 输入在任一方向上越远离0点，导数越接近0。\n\n## 三 ReLU 函数及其变体（半线性激活函数）\n\n### 3.1 ReLU 函数\n\n`ReLU`(Rectified Linear Unit，修正线性单元)，是目前深度神经网络中**最经常使用的激活函数**，它保留了类似 step 那样的生物学神经元机制: 输入超过阈值才会激发。公式如下所示:\n\n$$\nReLU(x) = max(0, x) = \\left \\lbrace \\begin{matrix}\nx & x\\geq 0 \\\\ \n0 & x< 0\n\\end{matrix}\\right.\n$$\n\n以上公式通俗理解就是，`ReLU` 函数仅保留正元素并丢弃所有负元素。注意: 虽然在 `0` 点不能求导，但是并不影响其在以梯度为主的反向传播算法中发挥有效作用。\n\n1，**优点**: \n\n- `ReLU` 激活函数**计算简单**；\n- 具有**很好的稀疏性**，大约 50% 的神经元会处于激活状态。\n- 函数在 x > 0 时导数为 1 的性质（**左饱和函数**），在一定程度上缓解了神经网络的梯度消失问题，加速梯度下降的收敛速度。\n> 相关生物知识: 人脑中在同一时刻大概只有 1% ∼ 4% 的神经元处于活跃状态。\n\n2，**缺点**: \n\n- ReLU 函数的输出是非零中心化的，给后一层的神经网络引入偏置偏移，会**影响梯度下降的效率**。\n- ReLU 神经元在训练时比较容易“死亡”。如果神经元参数值在一次不恰当的更新后，其值小于 0，那么这个神经元自身参数的梯度永远都会是 0，在以后的训练过程中永远不能被激活，这种现象被称作“**死区**”。\n\nReLU 激活函数的代码定义如下:\n\n```python\n# pytorch 框架对应函数： nn.ReLU(inplace=True)\nclass ReLU(object):\n\n    def func(self, x):\n        return np.maximum(x, 0.0)\n\n    def derivative(self, x):\n        \"\"\"简单写法: return x > 0.0\"\"\"\n        da = np.array([1 if x > 0 else 0 for x in a])\n        return da     \n```\n**ReLU 激活函数及其函数梯度图**如下所示:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/relu_and_gradient_curve2.png\" width=\"70%\" alt=\"relu_and_gradient_curve\">\n</div>\n\n> `ReLU` 激活函数的更多内容，请参考原论文 [Rectified Linear Units Improve Restricted Boltzmann Machines](https://www.cs.toronto.edu/~fritz/absps/reluICML.pdf)\n\n### 3.2，Leaky ReLU/PReLU/ELU/Softplus 函数\n\n1，`Leaky ReLU` **函数**: 为了缓解“**死区**”现象，研究者将 ReLU 函数中 x < 0 的部分调整为 $\\gamma \\cdot x$， 其中 $\\gamma$ 常设置为 0.01 或 0.001 数量级的较小正数。这种新型的激活函数被称作**带泄露的 ReLU**（`Leaky ReLU`）。\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + \\gamma\\ min(0, x)\n= \\left \\lbrace \\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma \\cdot x & x< 0\n\\end{matrix}\\right.\n$$\n\n> 详情可以参考原论文:[《Rectifier Nonlinearities Improve Neural Network Acoustic Models》](https://www.semanticscholar.org/paper/Rectifier-Nonlinearities-Improve-Neural-Network-Maas/367f2c63a6f6a10b3b64b8729d601e69337ee3cc?p2df)\n\n2，`PReLU` **函数**: 为了解决 Leaky ReLU 中**超参数 $\\gamma$ 不易设定**的问题，有研究者提出了参数化 ReLU(Parametric ReLU，`PReLU`)。参数化 ReLU 直接将 $\\gamma$ 也作为一个网络中可学习的变量融入模型的整体训练过程。对于第 $i$ 个神经元，`PReLU` 的 定义为:\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + \\gamma_{i}\\ min(0, x)\n= \\left\\lbrace\\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma_{i} \\cdot x & x< 0\n\\end{matrix}\\right.\n$$\n\n> 详情可以参考原论文:[《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》](https://arxiv.org/abs/1502.01852)\n\n3，`ELU` **函数**: 2016 年，Clevert 等人提出的 `ELU` (Exponential Linear Units) 在小于零的部分采用了负指数形式。`ELU`  有很多优点，一方面作为非饱和激活函数，它在所有点上都是连续的和可微的，所以不会遇到梯度爆炸或消失的问题；另一方面，与其他线性非饱和激活函数（如 ReLU 及其变体）相比，它有着更快的训练时间和更高的准确性。\n\n但是，与 ReLU 及其变体相比，其**指数操作也增加了计算量**，即模型推理时 `ELU` 的性能会比 `ReLU` 及其变体慢。 `ELU` 定义如下:\n\n$$\n\\text{Leaky ReLU}(x) = max(0, 𝑥) + min(0, \\gamma(exp(x) - 1)\n= \\left\\lbrace\\begin{matrix}\nx & x\\geq 0 \\\\ \n\\gamma(exp(x) - 1) & x< 0\n\\end{matrix}\\right.\n$$\n\n$\\gamma ≥ 0$ 是一个超参数，决定 $x ≤ 0$ 时的饱和曲线，并调整输出均值在 `0` 附近。\n\n> 详情可以参考原论文:[《Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)》](https://arxiv.org/abs/1511.07289)\n\n4，`Softplus` **函数**: Softplus 函数其导数刚好是 Logistic 函数.Softplus 函数虽然也具有单侧抑制、宽 兴奋边界的特性，却没有稀疏激活性。`Softplus` 定义为:\n\n$$\n\\text{Softplus}(x) = log(1 + exp(x))\n$$\n> 对 `Softplus` 有兴趣的可以阅读这篇论文: [《Deep Sparse Rectifier Neural Networks》](http://proceedings.mlr.press/v15/glorot11a/glorot11a.pdf)。\n\n注意: **ReLU 函数变体有很多，但是实际模型当中使用最多的还是 `ReLU` 函数本身**。\n\nReLU、Leaky ReLU、ELU 以及 Softplus 函数示意图如下图所示:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/relu_more.png\" width=\"70%\" alt=\"relu_more\">\n</div>\n\n## 四 Swish 函数\n\n`Swish` 函数[Ramachandran et al., 2017] 是一种自门控(Self-Gated)激活 函数，定义为\n\n$$\n\\text{swish}(x) = x\\sigma(\\beta x)\n$$\n\n其中 $\\sigma(\\cdot)$ 为 Logistic 函数，$\\beta$ 为可学习的参数或一个固定超参数。$\\sigma(\\cdot) \\in (0, 1)$ 可以看作一种软性的门控机制。当 $\\sigma(\\beta x)$ 接近于 `1` 时，门处于“开”状态，激活函数的输出近似于 $x$ 本身；当 $\\sigma(\\beta x)$ 接近于 `0` 时，门的状态为“关”，激活函数的输出近似于 `0`。\n\n`Swish` 函数代码定义如下：\n\n```python\n# Swish https://arxiv.org/pdf/1905.02244.pdf\nclass Swish(nn.Module):  #Swish激活函数\n    @staticmethod\n    def forward(x, beta = 1): # 此处beta默认定为1\n        return x * torch.sigmoid(beta*x)\n```\n\n结合前面的画曲线代码，可得 Swish 函数的示例图：\n\n<div align=\"center\">\n<img src=\"../images/activation_function/swish_of_different_beta2.png\" width=\"70%\" alt=\"Swish 函数\">\n</div>\n\n**Swish 函数可以看作线性函数和 ReLU 函数之间的非线性插值函数，其程度由参数 $\\beta$ 控制**。\n\n## 五 激活函数总结\n\n常用的激活函数包括 `ReLU` 函数、`sigmoid` 函数和 `tanh` 函数。其标准代码总结如下（Pytorch 框架中会更复杂）\n\n```python\nfrom math import exp\n\nclass Sigmoid(object):\n\n    def func(self, x):\n        return 1.0 / (1.0 + np.exp(-x))\n\n    def derivative(self, x):\n        return self.func(x) * (1.0 - self.func(x))\n\nclass Tanh(object):\n\n    def func(self, x):\n        return np.tanh(x)\n\n    def derivative(self, x):\n        return 1.0 - self.func(x) ** 2\n    \nclass ReLU(object):\n\n    def func(self, x):\n        return np.maximum(x, 0.0)\n\n    def derivative(self, x):\n        return x > 0.0\n\nclass LeakyReLU(object):\n\n    def __init__(self, alpha=0.2):\n        super().__init__()\n        self.alpha = alpha\n\n    def func(self, x):\n        return np.array([x if x > 0 else self.alpha * x for x in z])\n\n    def derivative(self, x):\n        dx = np.array([1 if x > 0 else self.alpha for x in a])\n        return dx\n\nclass Softplus(object):\n\n    def func(self, x):\n        return np.log(1 + np.exp(z))\n\n    def derivative(self, x):\n        return 1.0 / (1.0 + np.exp(-x))\n```\n\n下表汇总比较了几个激活函数的属性:\n\n<div align=\"center\">\n<img src=\"../images/activation_function/activation_function_summary.png\" width=\"100%\" alt=\"activation_function\">\n</div>\n\n**激活函数的在线可视化**移步 [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)。\n\n## 参考资料\n\n1. [Pytorch分类问题中的交叉熵损失函数使用](https://www.cnblogs.com/hmlovetech/p/14515622.html)\n2. 《解析卷积神经网络-第8章》\n3. 《神经网络与深度学习-第4章》\n4. [How to Choose an Activation Function for Deep Learning](https://machinelearningmastery.com/choose-an-activation-function-for-deep-learning/)\n5. [深度学习中的激活函数汇总](http://spytensor.com/index.php/archives/23/)\n6. [Visualising Activation Functions in Neural Networks](https://dashee87.github.io/deep%20learning/visualising-activation-functions-in-neural-networks/)\n7. [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)\n8. https://github.com/borgwang/tinynn/blob/master/tinynn/core/layer.py"
  },
  {
    "path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-tensor数据结构.md",
    "content": "---\nlayout: post\ntitle: Pytorch基础-tensor数据结构\ndate: 2021-03-07 12:00:00\nsummary: torch.Tensor 是一种包含单一数据类型元素的多维矩阵，类似于 numpy 的 array，本文详细介绍了 Tensor 的数据类型、属性以及如何创建。\ncategories: DeepLearning\n---\n\n- [Tensor 概述](#tensor-概述)\n  - [1.1 Tensor 数据类型](#11-tensor-数据类型)\n  - [1.2 Tensor 的属性](#12-tensor-的属性)\n- [二 创建 Tensor](#二-创建-tensor)\n  - [2.1 传数据的方法创建 Tensor](#21-传数据的方法创建-tensor)\n  - [2.2 传 size 的方法创建 Tensor](#22-传-size-的方法创建-tensor)\n  - [2.3 其他创建 tensor 的方法](#23-其他创建-tensor-的方法)\n  - [2.4 代码示例](#24-代码示例)\n  - [创建张量方法总结](#创建张量方法总结)\n- [参考资料](#参考资料)\n\n## Tensor 概述\n\n`torch.Tensor` 是一种包含**单一数据类型**元素的多维矩阵，类似于 numpy 的 `array`。\n\n![tensor](../../images//tensor_datastructure/tensor.png)\n\n1，指定数据类型的 tensor 可以通过传递参数 `torch.dtype` 和/或者 `torch.device` 到构造函数生成：\n> 注意为了改变已有的 tensor 的 torch.device 和/或者 torch.dtype, 考虑使用 `to()` 方法.\n\n```python\n>>> torch.ones([2,3], dtype=torch.float64, device=\"cuda:0\")\ntensor([[1., 1., 1.],\n        [1., 1., 1.]], device='cuda:0', dtype=torch.float64)\n>>> torch.ones([2,3], dtype=torch.float32)\ntensor([[1., 1., 1.],\n        [1., 1., 1.]])\n```\n\n2，Tensor 的内容可以通过 Python 索引或者切片访问以及修改，用法和 `ndarray` 的操作一致：\n\n```python\n>>> matrix = torch.tensor([[2,3,4],[5,6,7]])\n>>> print(matrix[1][2])\ntensor(7)\n>>> matrix[1][2] = 9\n>>> print(matrix)\ntensor([[2, 3, 4],\n        [5, 6, 9]])\n```\n\n3，使用 `torch.Tensor.item()` 或者 `int()` 方法从**只有一个值的 Tensor**中获取 Python 数值对象：\n\n```python\n>>> x = torch.tensor([[4.5]])\n>>> x\ntensor([[4.5000]])\n>>> x.item()\n4.5\n>>> int(x)\n4\n```\n\n4，Tensor可以通过参数 `requires_grad=True` 创建, 这样 `torch.autograd` 会记录相关的运算实现自动求导：\n\n```python\n>>> x = torch.tensor([[1., -1.], [1., 1.]], requires_grad=True)\n>>> out = x.pow(2).sum()\n>>> out.backward()\n>>> x.grad\ntensor([[ 2.0000, -2.0000],\n [ 2.0000,  2.0000]])\n```\n\n5，每一个 tensor都有一个相应的 `torch.Storage` 保存其数据。tensor 类提供了一个多维的、strided 视图, 并定义了数值操作。\n\n6，张量和 numpy 数组。可以用 `.numpy()` 方法从 Tensor 得到 numpy 数组，也可以用 `torch.from_numpy` 从 numpy 数组得到Tensor。这两种方法关联的 Tensor 和 numpy 数组是共享数据内存的。可以用张量的 `clone`方法拷贝张量，中断这种关联。\n\n```python\narr = np.random.rand(4,5)\nprint(type(arr))\ntensor1 = torch.from_numpy(arr)\nprint(type(tensor1))\narr1 = tensor1.numpy()\nprint(type(arr1))\n\"\"\"\n<class 'numpy.ndarray'>\n<class 'torch.Tensor'>\n<class 'numpy.ndarray'>\n\"\"\"\n```\n\n7，2，`item()` 方法和 `tolist()` 方法可以将张量转换成 Python 数值和数值列表\n\n```python\n# item方法和tolist方法可以将张量转换成Python数值和数值列表\nscalar = torch.tensor(5)  # 标量\ns = scalar.item()\nprint(s)\nprint(type(s))\n\ntensor = torch.rand(3,2)  # 矩阵\nt = tensor.tolist()\nprint(t)\nprint(type(t))\n\"\"\"\n1.0\n<class 'float'>\n[[0.8211846351623535, 0.20020723342895508], [0.011571824550628662, 0.2906131148338318]]\n<class 'list'>\n\"\"\"\n```\n\n### 1.1 Tensor 数据类型\n\nTorch 定义了七种 CPU Tensor 类型和八种 GPU Tensor 类型：\n\n![tensor数据类型](../../images//tensor_datastructure/tensor_data_types.png)\n\n`torch.Tensor` 是默认的 tensor 类型（`torch.FloatTensor`）的简称，即 `32` 位浮点数数据类型。\n\n### 1.2 Tensor 的属性\n\nTensor 有很多属性，包括数据类型、Tensor 的维度、Tensor 的尺寸。\n\n+ **数据类型**：可通过改变 torch.tensor() 方法的 `dtype` 参数值，来设定不同的 `Tensor` 数据类型。\n+ **维度**：不同类型的数据可以用不同维度(dimension)的张量来表示。标量为 `0` 维张量，向量为 `1` 维张量，矩阵为 `2` 维张量。彩色图像有 `rgb` 三个通道，可以表示为 `3` 维张量。视频还有时间维，可以表示为 `4` 维张量，有几个中括号 `[` 维度就是几。**可使用 `dim() 方法` 获取 `tensor` 的维度**。\n+ **尺寸**：可以使用 `shape属性`或者 `size()方法`查看张量在每一维的长度，可以使用 `view()方法`或者`reshape() 方法`改变张量的尺寸。Pytorch 框架中四维张量形状的定义是 `(N, C, H, W)`。\n+ **张量元素总数**：numel() 方法返回（输入）张量元素的总数。\n+ **设备**：`.device` 返回张量所在的设备。\n\n> 关于如何理解 Pytorch 的 Tensor Shape 可以参考 stackoverflow 上的这个 [回答](https://stackoverflow.com/questions/52370008/understanding-pytorch-tensor-shape)。\n\n样例代码如下：\n\n```python\nmatrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],\n                       [[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)\nprint(matrix)               # 打印 tensor\nprint(matrix.dtype)     # 打印 tensor 数据类型\nprint(matrix.dim())     # 打印 tensor 维度\nprint(matrix.size())     # 打印 tensor 尺寸\nprint(matrix.shape)    # 打印 tensor 尺寸\nmatrix2 = matrix.view(4, 2, 2) # 改变 tensor 尺寸\nprint(matrix2)\nprint(matrix.numel())\n```\n```python\n>>> matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],\n...                        [[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)\n>>> matrix\ntensor([[[1., 2., 3., 4.],\n         [5., 6., 7., 8.]],\n\n        [[5., 4., 6., 7.],\n         [5., 6., 8., 9.]]], dtype=torch.float64)\n>>> matrix.dtype        # tensor 数据类型\ntorch.float64\n>>> matrix.dim()        # tensor 维度\n3\n>>> matrix.size()       # tensor 尺寸\ntorch.Size([2, 2, 4])\n>>> matrix.shape        # tensor 形状\ntorch.Size([2, 2, 4])\n>>> matrix.numel()      # tensor 元素总数\n16\n>>> matrix.device\ndevice(type='cpu')\n```\n\n## 二 创建 Tensor\n\n创建 tensor ，可以传入数据或者维度，torch.tensor() 方法只能传入数据，torch.Tensor() 方法既可以传入数据也可以传维度，强烈建议 tensor() 传数据，Tensor() 传维度，否则易搞混。\n\n具体来说，一般使用 torch.tensor() 方法将 python 的 `list` 或 numpy 的 `ndarray` 转换成 Tensor 数据，生成的是`dtype`  默认是 `torch.FloatTensor`，和 torch.float32 或者 torch.float 意义一样。\n\n通过 torch.tensor() 传入数据的方法创建 tensor 时，`torch.tensor()` 总是拷贝 data 且一般不会改变原有数据的数据类型 `dtype`。如果你有一个 tensor data 并且仅仅想改变它的 `requires_grad` 属性，可用 `requires_grad_()` 或者 `detach()` 来避免拷贝。如果你有一个 `numpy` 数组并且想避免拷贝，请使用 `torch.as_tensor()`。\n\n### 2.1 传数据的方法创建 Tensor\n\n1，`torch.tensor()`。\n\n将 python 的 `list` 或 numpy 的 `ndarray` 转换成 Tensor 数据。\n```python\ntorch.tensor(data, dtype=None, device=None, requires_grad=False, pin_memory=False)\n```\n参数解释：\n- `data`: 数据，可以是 list，ndarray\n- `dtype`: 数据类型，默认与 data 的一致\n- `device`: 所在设备，cuda/cpu\n- `requires_grad`: 是否需要梯度\n- `pin_memory`: 是否存于锁页内存\n\n代码示例：\n\n```python\n>>> a = np.arange(12).reshape(3,4)\n>>> b = torch.tensor(a)\n>>> b        # 打印张量 b 数据\ntensor([[ 0,  1,  2,  3],\n        [ 4,  5,  6,  7],\n        [ 8,  9, 10, 11]])\n>>> a.dtype\ndtype('int64')\n>>> b.dtype # 张量 b 元素的数据类型（和 numpy 数组 a 一致 ）\ntorch.int64\n>>> a.shape \n(3, 4)\n>>> b.shape # 张量 b 的形状\ntorch.Size([3, 4])\n>>> b.dim() # 张量 b 的维度\n2\n>>> b.numel() # 张量 b 的元素个数\n12\n```\n\n2，`torch.from_numpy(ndarray)`\n\n从 numpy 创建 tensor。利用这个方法创建的 tensor 和原来的 ndarray 共享内存（不会拷贝数据，节省内存和时间），当修改其中一个数据，另外一个也会被改动。\n\n```python\n>>> arr = np.arange(1, 7).reshape(2, 3)\n>>> ta = torch.from_numpy(arr)\n>>> ta[1][2] = 100 # 修改第 2 行 第 3 列元素值为 100\n>>> ta\ntensor([[  1,   2,   3],\n        [  4,   5, 100]])\n>>> arr\narray([[  1,   2,   3],\n       [  4,   5, 100]])\n```\n\n3，`torch.empty_like`、`torch.zeros_like`、`torch.ones_like` 和 `torch.randint_like()`。\n\n- 前面三个函数是根据 input（tensor 数据） 形状创建空、全 0 和全 1 的张量。\n- `torch.randint_like()`：返回和输入 tensor 形状相同的张量，但是数据范围由输入参数指定，在 `[low=0, high]` 之间均匀生成的随机整数。\n\n`torch.empty_like` 和 `torch.randint_like()` 函数声明如下所示：\n\n```python\ntorch.empty_like(input, *, dtype=None,) -> Tensor\n# torch.ones_like 和 torch.zeros_like 几乎一致\ntorch.zeros_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor\ntorch.randint_like(input, low=0, high, \\*, dtype=None, layout=torch.strided, device=None, requires_grad=False, memory_format=torch.preserve_format) → Tensor\n```\n\n`torch.empty_like` 和 `torch.zeros_like` 的简单函数实例如下所示：\n\n```python\narr = np.arange(20).reshape(5, 4)\na_tensor = torch.from_numpy(arr)\nb_like = torch.empty_like(a_tensor)\nc_like = torch.zeros_like(a_tensor)\nassert a_tensor.shape == b_like.shape == c_like.shape == torch.Size([5,4])\nprint(c_like.shape)\nprint(c_like)\n\"\"\"\ntorch.Size([5, 4])\ntensor([[0, 0, 0, 0],\n        [0, 0, 0, 0],\n        [0, 0, 0, 0],\n        [0, 0, 0, 0],\n        [0, 0, 0, 0]])\n\"\"\"\n```\n\n### 2.2 传 size 的方法创建 Tensor\n\n1，`torch.empty`、`torch.zeros`、`torch.ones` 等方法。\n\n直接传入张量形状即可创建形状为 `size` 的空、全 0 和全 1 的张量。\n\n```python\ntorch.zeros(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)\n```\n\n代码示例：\n```python\n>>> torch.zeros(4,5,6)\ntensor([[[0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.]],\n\n        [[0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.]],\n\n        [[0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.]],\n\n        [[0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.],\n         [0., 0., 0., 0., 0., 0.]]])\n```\n\n### 2.3 其他创建 tensor 的方法\n\n1，`torch.arange()`\n\n功能和参数 `np.arange()` 几乎一样，默认创建区间为 `[0, end)` 公差为 $1$ 的 1维张量。\n\n```python\ntorch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)\n```\n参数解释：\n- start: 数列起始值\n- end: 数列结束值，开区间，取不到结束值\n- step: 数列公差，默认为 `1`\n\n2，`torch.normal()`\n\n根据给定的均值 (mean=0) 和标准差 (std=1) 从正态分布中抽取随机样本。每个生成的元素都是独立的随机变量，**遵循相同的正态分布**。\n\n值的注意的是，当我们生成一个有限大小的样本（例如 3x3 共9个元素）时，样本的均值和标准差并不一定严格等于总体的均值和标准差。这是因为：\n\n- 抽样误差（Sampling Error）: 由于样本量有限，样本统计量（如均值、标准差）可能会偏离总体参数。这种偏差是自然的随机现象，随着样本量的增加，样本统计量会更接近总体参数。\n- 有限样本量的波动: 在小样本中，随机波动对统计量的影响较大。例如，在仅有 9 个样本的情况下，个别极端值可能显著影响均值和标准差。\n\n**参数说明**：\n- `mean` (float 或 Tensor): 正态分布的均值。如果是张量，必须与 std 的形状一致。\n- `std` (float 或 Tensor): 正态分布的标准差。如果是张量，必须与 mean 的形状一致。\n- `size` (tuple): 输出张量的形状。\n- `generator` (torch.Generator, 可选): 用于生成随机数的随机数生成器。\n- `out` (Tensor, 可选): 用于存储输出结果的张量。\n返回一个张量，张量中的随机数从各自的正态分布中抽取，这些正态分布的均值和标准差是给定的。\n\n这个函数有 4 种模式，这里给出常见 `torch.normal(mean=float, std=float, size, *)` 的用法示例。\n\n```python\ntorch.normal(mean, std, size, *, out=None) → Tensor\n```\n\n参数解释:\n- `mean`（float）：所有分布的均值\n- `std`（float）：所有分布的标准差\n- `size`（int…）：定义输出张量形状的整数序列\n\n3，`torch.randn()` 或 `torch.randn_like()`\n\n功能：返回一个形状为 `size` 的张量，张量中的随机数来自均值为 0、方差为 1 的正态分布（也称为标准正态分布）。\n\n```python\ntorch.randn(*size, *, generator=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor\n```\n\n4，`torch.randint()` 和 `torch.randint_like()`\n\n功能：**返回一个与 Tensor input 形状相同的张量，其中填充了区间 `[low, high)` 之间均匀生成的随机整数。**\n\n```python\ntorch.randint（low=0， high， size， *， generator=None， out=None， dtype=None， layout=torch.strided， device=None， requires_grad =False）-> Tensor\n```\n\n5，`torch.tril` 和 `torch.tril` (triu 和 tril 分别是“triangle upper”和“triangle lower”的缩写。)\n\n```python\ntorch.tril(input, diagonal=0, *, out=None) → Tensor\n```\n\n- `torch.tril` 函数返回输入矩阵（2D 张量）或一组矩阵的**下三角部分**，结果张量中的其余元素被设置为 0。下三角部分包含矩阵的主对角线及其以下的元素。\n- `torch.triu` 函数返回一个矩阵的上三角部分（即矩阵上半部分，主对角线及其以上的元素），其余部分设为零。\n\n参数 `diagonal` 控制要保留的对角线。如果 diagonal = 0，则保留主对角线及其以下的所有元素。正值的 diagonal 会包括主对角线上方的对角线，负值则排除主对角线下方的对角线，即表示向上或向下偏移的对角线。主对角线的索引为  ${(i, i)}$ ，其中 $i \\in [0, \\min \\{d_1, d_2\\} - 1]$， $d_1$ 和 $d_2$ 分别为矩阵的维度。\n\n### 2.4 代码示例\n\n```python\n>>> torch.arange(12).reshape(4,3)               # torch.arange() 用法\ntensor([[ 0,  1,  2],\n        [ 3,  4,  5],\n        [ 6,  7,  8],\n        [ 9, 10, 11]])\n>>> torch.normal(mean=0, std=2, size=[1,4])    # torch.normal() 用法\ntensor([[ 0.6018, -0.2399,  2.8425,  1.6153]]) # torch.randn() 用法\n>>> torch.randn(4,6,2)\ntensor([[[ 1.3282, -0.0920],\n         [ 0.4889,  0.0805],\n         [-0.5224, -0.5830],\n         [-0.7645, -0.6670],\n         [ 0.2376,  0.0135],\n         [-0.3824,  0.1190]],\n\n        [[ 1.0024, -1.6934],\n         [ 0.2822, -0.1121],\n         [ 0.1233,  0.4210],\n         [ 1.5558,  1.1571],\n         [-1.9819, -1.0007],\n         [ 1.7181,  0.5641]],\n\n        [[ 0.2924,  0.7369],\n         [ 0.4954, -2.3034],\n         [-1.1726,  0.7474],\n         [-0.1254, -0.2139],\n         [-0.3428,  1.2906],\n         [ 1.2389, -0.5154]],\n\n        [[ 0.8589,  2.7191],\n         [-0.0905,  0.3279],\n         [ 1.8878,  0.6622],\n         [-0.1519,  0.4263],\n         [-0.9688,  1.2181],\n         [-2.0909, -0.3234]]])\n>>> torch.randint(10, (2,5))           # torch.randint() 用法\ntensor([[3, 8, 7, 3, 5],\n        [9, 2, 2, 9, 6]])\n>>> torch.randn(4,5).tril()            # 创建下三角矩阵\ntensor([[-0.8062,  0.0000,  0.0000,  0.0000,  0.0000],\n        [ 1.4425,  0.8054,  0.0000,  0.0000,  0.0000],\n        [ 0.9237,  0.8548,  0.2412,  0.0000,  0.0000],\n        [-0.3921, -0.4880, -0.8847, -0.2170,  0.0000]])\n>>> torch.randn(4,5).triu()           # 创建上三角矩阵\ntensor([[-0.7354, -0.9383, -0.0798, -0.6155,  0.3702],\n        [ 0.0000,  0.5686,  0.2166, -0.5724, -0.0596],\n        [ 0.0000,  0.0000,  0.0751, -0.9832,  1.5582],\n        [ 0.0000,  0.0000,  0.0000, -0.0491,  1.3136]])\n```\n\n检验 torch.normal 和 torch.randn 的均值情况：\n\n```python\nimport torch\n\ndef generate_tensor_and_print_stats(size, func=\"normal\", mean=0.0, std=1.0):\n    \"\"\"\n    生成一个张量并打印其均值和标准差。\n\n    参数:\n    - size (tuple): 生成张量的形状。\n    - func (str): 使用的生成函数。选项包括 \"normal\" 和 \"randn\"。\n    - mean (float, 可选): 正态分布的均值，仅在 func=\"normal\" 时使用。默认值为0.0。\n    - std (float, 可选): 正态分布的标准差，仅在 func=\"normal\" 时使用。默认值为1.0。\n\n    返回:\n    - tensor (Tensor): 生成的张量。\n    \"\"\"\n    if func == \"normal\":\n        tensor = torch.normal(mean=mean, std=std, size=size)\n    elif func == \"randn\":\n        tensor = torch.randn(size=size)\n    else:\n        raise ValueError(f\"无效的函数类型: {func}. 请使用 'normal' 或 'randn'。\")\n    \n    tensor_mean = tensor.mean().item()\n    tensor_std = tensor.std().item()\n    \n    print(f\"Tensor Size: {size}, by torch.{func}, Mean: {tensor_mean:.4f}, Std Dev: {tensor_std:.4f}\")\n    return tensor\n\ndef main():\n    # 设置随机种子以确保结果可重复（可选）\n    torch.manual_seed(40)\n\n    # 定义不同的张量大小\n    sizes = [(3, 3), (100, 100), (1000, 1000)]\n    functions = [\"normal\", \"randn\"]\n\n    # 生成并打印统计信息\n    for func in functions:\n        for size in sizes:\n            if func == \"normal\":\n                generate_tensor_and_print_stats(size=size, func=func, mean=0.0, std=1.0)\n            elif func == \"randn\":\n                generate_tensor_and_print_stats(size=size, func=func)\n\nif __name__ == \"__main__\":\n    main()\n```\n\n程序运行后输出结果如下所示:\n\n```bash\nTensor Size: (3, 3), by torch.normal, Mean: 0.4151, Std Dev: 0.6608\nTensor Size: (100, 100), by torch.normal, Mean: -0.0135, Std Dev: 0.9947\nTensor Size: (1000, 1000), by torch.normal, Mean: 0.0002, Std Dev: 1.0006\n\nTensor Size: (3, 3), by torch.randn, Mean: -0.7619, Std Dev: 1.1099\nTensor Size: (100, 100), by torch.randn, Mean: -0.0133, Std Dev: 1.0029\nTensor Size: (1000, 1000), by torch.randn, Mean: -0.0001, Std Dev: 1.0001\n```\n\n解释：\n- $3\\times 3$ 张量: 样本均值和 0、标准差和 1 比都有较大的偏差。\n- $100\\times 100$ 张量: 偏差减小，均值更接近0，标准差更接近1。\n- $1000\\times 1000$ 张量: 偏差进一步减小，**均值非常接近0，标准差非常接近1**。\n\n### 创建张量方法总结\n\n|方法名|方法功能|备注|\n|-----|-------|---|\n|`torch.rand(*sizes, out=None) → Tensor`|返回一个张量，包含了从区间 `[0, 1)` 的**均匀分布**中抽取的一组随机数。张量的形状由参数sizes定义。|推荐|\n|`torch.randn(*sizes, out=None) → Tensor`|返回一个张量，包含了从**标准正态分布**（均值为0，方差为1，即高斯白噪声）中抽取的一组随机数。张量的形状由参数sizes定义。|不推荐|\n|`torch.normal(means, std, out=None) → Tensor`|返回一个张量，包含了从指定均值 `means` 和标准差 `std` 的离散正态分布中抽取的一组随机数。标准差 `std` 是一个张量，包含每个输出元素相关的正态分布标准差。|多种形式，建议看源码|\n|`torch.rand_like(a)`|根据数据 `a` 的 shape 来生成随机数据|不常用|\n|`torch.randint(low=0, high, size)`|生成指定范围(`low, hight`)和 `size` 的随机整数数据|常用|\n|`torch.full([2, 2], 4)`|生成给定维度，全部数据相等的数据|不常用|\n|`torch.arange(start=0, end, step=1, *, out=None)`|生成指定间隔的数据|易用常用|\n|`torch.ones(*size, *, out=None)`|生成给定 size 且值全为1 的矩阵数据|简单|\n|`zeros()/zeros_like()/eye()`|全 `0` 的 tensor 和 对角矩阵|简单|\n\n样例代码：\n\n```python\n>>> torch.rand([1,1,3,3])\ntensor([[[[0.3005, 0.6891, 0.4628],\n          [0.4808, 0.8968, 0.5237],\n          [0.4417, 0.2479, 0.0175]]]])\n>>> torch.normal(2, 3, size=(1, 4))\ntensor([[3.6851, 3.2853, 1.8538, 3.5181]])\n>>> torch.full([2, 2], 4)\ntensor([[4, 4],\n        [4, 4]])\n>>> torch.arange(0,10,2)\ntensor([0, 2, 4, 6, 8])\n>>> torch.eye(3,3)\ntensor([[1., 0., 0.],\n        [0., 1., 0.],\n        [0., 0., 1.]])\n```\n\n## 参考资料\n\n+ [PyTorch：view() 与 reshape() 区别详解](https://blog.csdn.net/Flag_ing/article/details/109129752)\n+ [torch.rand和torch.randn和torch.normal和linespace()](https://zhuanlan.zhihu.com/p/115997577)"
  },
  {
    "path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-张量数学运算.md",
    "content": "---\nlayout: post\ntitle: Pytorch基础-张量数学运算\ndate: 2021-03-03 12:00:00\nsummary: PyTorch 张量数学运算就是对张量的元素值完成数学运算，常用的张量数学运算包括：标量运算、向量运算、矩阵运算。\ncategories: DeepLearning\n---\n\n- [一 理解张量维度](#一-理解张量维度)\n- [二 理解 dim 参数](#二-理解-dim-参数)\n- [三 规约计算](#三-规约计算)\n  - [3.1 torch.mean](#31-torchmean)\n  - [3.2 torch.sum](#32-torchsum)\n  - [3.3 torch.max](#33-torchmax)\n  - [3.4 torch.cumprod](#34-torchcumprod)\n\nPyTorch 张量数学运算就是对张量的元素值完成数学运算，常用的张量数学运算包括：标量运算、向量运算、矩阵运算。\n\n## 一 理解张量维度\n\n在 PyTorch 中，张量的维度（或称为“秩”）决定了数据的结构和形状：\n\n- 1D 张量：向量。例如，长度为 5 的向量 [1, 2, 3, 4, 5]。\n- 2D 张量：矩阵。例如，形状为 (3, 4) 的矩阵。\n- 3D 张量：通常用于 NLP，形状为 (batch_size, sequence_length, hidden_size)。\n- 4D 张量：通常用于 CV，形状为 (batch_size, channels, height, width)。\n\n在一个 $M$ 行 $N$ 列的二维数组中，$M$ 是第 0 维，即行数；$N$ 是第 1 维，即列数。那么怎么肉眼判断更复杂的张量数据维度呢，举例：\n\n```python\nimport torch\n\n# 示例张量\ntensor = torch.tensor([[[0.6238, -0.9315, 0.2173, 0.1954, -1.1565],\n                        [0.4559, 0.1531, 0.4178, 1.0225, 0.5923],\n                        [0.0499, 0.4024, -1.2547, -0.5042, -0.0231],\n                        [-1.1253, 0.3145, 0.8796, 0.4516, -0.0915]],\n\n                       [[1.5794, -0.6367, -0.2559, 0.1237, -0.1951],\n                        [0.1012, 0.0357, -0.5699, 1.0983, -0.2084],\n                        [-0.7019, 0.5872, 0.7736, 0.7423, -0.7894],\n                        [-0.3248, -0.5316, 1.2029, 0.2852, -0.4565]],\n\n                       [[-0.0073, 1.4143, -0.1859, -0.7211, -0.8652],\n                        [-0.3173, -0.4816, 0.1174, -0.1554, 0.9385],\n                        [0.1283, -0.6547, 0.3687, -0.1948, 0.7754],\n                        [-0.2185, -1.0437, 1.5963, -0.3284, -0.3654]]])\n```\n**判断规则**：方括号 `[` 的嵌套层数代表张量的维度。最外层括号的元素数量是第 0 维的大小，往内推。\n以上述张量为例分析：\n```python\ntensor([[[ 0.6238, -0.9315,  0.2173,  0.1954, -1.1565], ... ]])\n```\n- 最外层 [ 里有 3 个子列表 -> 第 0 维大小为 3。\n- 第二层 [ 里有 4 个子列表 -> 第 1 维大小为 4。\n- 第三层 [ 里有 5 个元素 -> 第 2 维大小为 5。\n\n因此，这个张量是 3 维张量，形状为 `[3, 4, 5]`。\n\n## 二 理解 dim 参数\n\n**`dim` 参数在 pytorch 数学函数中的定义一般指沿着 dim 这个维度进行操作**：求和/求平均/求累加，以及删除、增加指定 dim。如 x 的 shape 为 [2, 5, 3]，则：\n- `dim = 0`，即沿着具有 2 个元素的那个维度/轴进行操作\n- `dim = 1`，即沿着具有 5 个元素的那个维度/轴进行操作\n- `dim = 2`，即沿着具有 3 个元素的那个维度/轴进行操作\n\n再看具体示例：\n```bash\n>>> x = torch.randint(1, 10, [2,5,3], dtype=torch.float32)\n>>> x\ntensor([[[8., 1., 6.],\n         [1., 5., 9.],\n         [5., 7., 1.],\n         [5., 1., 2.],\n         [8., 4., 4.]],\n\n        [[3., 3., 7.],\n         [7., 4., 3.],\n         [7., 7., 5.],\n         [8., 3., 9.],\n         [1., 1., 8.]]])\n>>> x.shape\ntorch.Size([2, 5, 3])\n\n# 如执行 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\n>>> y = torch.mean(x, dim = 0)\n>>> y\ntensor([[5.5000, 2.0000, 6.5000],\n        [4.0000, 4.5000, 6.0000],\n        [6.0000, 7.0000, 3.0000],\n        [6.5000, 2.0000, 5.5000],\n        [4.5000, 2.5000, 6.0000]])\n\n# 如执行 y = torch.mean(x, dim = 2)， 则 y[1, 4] = (x[1, 4, 0] + x[1, 4, 1] + x[1, 4, 2]) / 3\n>>> y = torch.mean(x, dim = 2)\n>>> y\ntensor([[5.0000, 5.0000, 4.3333, 2.6667, 5.3333],\n        [4.3333, 4.6667, 6.3333, 6.6667, 3.3333]])\n```\n\n## 三 规约计算\n\n规约计算一般是指分组聚合计算，表现结果就是会进行维度压缩。\n\n### 3.1 torch.mean\n\ntorch.mean 函数用于计算张量沿指定维度的平均值。其基本语法和参数解释如下：\n\n```python\ntorch.mean(input, dim, keepdim=False, *, dtype=None) -> Tensor\n```\n- `input`：输入张量。\n- `dim`：沿哪个维度计算平均值。可以是单个整数或整数元组。\n- `keepdim`：是否保留被缩减的维度。默认为 False。\n- `dtype`：输出张量的数据类型。\n\n1. 从计算过程理解：**沿着（跨） dim 进行操作（算均值）**。\n   - 常规矩阵操作的 2D 张量，dim = 0 表示跨行操作，即对每一列中的所有元素进行均值计算。\n   - 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}$。\n   - 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}$。\n\n2. 从输出张量的形状理解：\n   - NLP 领域的 3D 张量，dim = 2，输出张量的形状去掉这个 dim 维度，得到输出张量形状为 `(batch_size, sequence_length)`。\n   - CV 领域的 4D 张量，dim = 0，输出张量形状为 `(channels, height, width)`。\n3. 是否保留维度参数的理解：\n\t- keepdim=False（默认）：求和后，指定的维度会被压缩，即结果张量的维度将减少。\n\t- keepdim=True：求和后，指定的维度将保留，且其大小为 1，这对于后续需要特定维度的操作（如广播机制）非常有用。\n  \n### 3.2 torch.sum\n\n`torch.sum` 沿着指定维度求和。\n```bash\n>>> x = torch.randn([4,5])\n>>> x\ntensor([[ 1.1141,  1.7091, -0.5543,  0.3417, -0.0838],\n        [-0.6697, -0.3165,  0.3772, -0.4377, -0.9850],\n        [-1.3976,  1.3172, -0.6791,  0.1030, -0.5817],\n        [ 0.3079, -0.5911,  1.2357, -1.0891,  0.8422]])\n>>> x.sum(dim=0)\ntensor([-0.6453,  2.1187,  0.3796, -1.0821, -0.8082])\n>>> x.sum(dim=1)\ntensor([ 2.5269, -2.0317, -1.2381,  0.7056])\n```\n\n当 dim = 0 时，就是沿着 `dim = 0`即 `x` 轴进行累加，sum 函数为规约函数会压缩维度，所以x.sum(dim=0) 结果为 tensor([-0.6453,  2.1187,  0.3796, -1.0821, -0.8082])，形状为 `[5]`。\n\n### 3.3 torch.max\n\n`torch.max()` 用于获取张量的最大值，其有三种用法：\n\n- 获取张量中的最大值。\n- 沿指定维度获取最大值及其索引。\n- 逐元素比较两个张量，返回最大值，用法等同 `torch.minimum()` 函数\n\n这三种用法的各自语法如下:\n\n```python\ntorch.max(input) -> Tensor\ntorch.max(input, dim, keepdim=False, *, out=None)\ntorch.min(input, other, *, out=None) -> Tensor # other 也是张量\n```\n\n- 第一种用法好理解，输入参数是一个多维张量，返回的结果是这个张量的**全局最大值**！\n- 第二种情况是适用于我们需要特定维度的最大值，且返回结果包含两个数据：一个是最大值对应的位置索引，一个是最大值本身，这个函数是量化算法的核心操作之一！\n- 第三种情况，用于对输入的两个张量 `input` 和 `other`中的每个元素进行比较，返回对应位置的最小值。\n\n```python\n>>> x = torch.randint(10, [2,5])\n>>> x\ntensor([[1, 6, 5, 7, 7],\n        [5, 3, 2, 2, 6]])\n>>> y = torch.randint(2, 12, [2,5])\n>>> y\ntensor([[ 8,  3,  8,  7,  4],\n        [11,  6,  2,  5,  3]])\n>>> torch.max(x, y)\ntensor([[ 8,  6,  8,  7,  7],\n        [11,  6,  2,  5,  6]])\n>>> torch.max(x)\ntensor(7)\n>>> max_x = torch.max(x, dim=0)  # 返回张量 x 在 dim=0 维度的最大值及对应索引\n>>> max_x\ntorch.return_types.max(\nvalues=tensor([5, 6, 5, 7, 7]),\nindices=tensor([1, 0, 0, 0, 0]))\n>>> max_x[0]\ntensor([5, 6, 5, 7, 7])\n```\n\n`torch.min` 函数和 `torch.max()` 意义相同，只不过返回的是最小值。\n\n### 3.4 torch.cumprod\n\n`torch.cumprod` 张量沿着指定 dim 维度计算累积乘积，其返回一个与 `input` 形状相同的张量，返回张量的每个元素是指定维度上该元素及其之前所有元素的乘积。函数定义(语法)如下:\n\n```python\ntorch.cumprod(input, dim, *, dtype=None, out=None) -> Tensor\n```\n\n这个沿着 dim 方向计算累积的计算过程直观上不好理解，可以参考下述计算过程的可视化分解图来理解。\n\n<div align=\"center\">\n<img src=\"../images/pytorch_tensor_math/dim_compute_visual.png\" width=\"80%\" alt=\"dim_compute_visual\">\n</div>\n\n多维张量的示例代码如下所示:\n\n```python\nimport torch\n\n# 创建一个 2D 张量\nb = torch.tensor([[1, 2, 3], [4, 5, 6]])\nprint(\"原始张量：\\n\", b)\n\n# 沿第 0 维计算累积乘积\ncumprod_b_dim0 = torch.cumprod(b, dim=0)\nprint(\"沿第 0 维的累积乘积结果：\\n\", cumprod_b_dim0)\n\n# 沿第 1 维计算累积乘积\ncumprod_b_dim1 = torch.cumprod(b, dim=1)\nprint(\"沿第 1 维的累积乘积结果：\\n\", cumprod_b_dim1)\n```\n\n程序运行后输出结果如下:\n\n```bash\n原始张量：\n tensor([[1, 2, 3],\n        [4, 5, 6]])\n沿第 0 维的累积乘积结果：\n tensor([[ 1,  2,  3],\n        [ 4, 10, 18]])\n沿第 1 维的累积乘积结果：\n tensor([[  1,   2,   6],\n        [  4,  20, 120]])\n```\n\n总结：上述 Tensor 数学计算函数的用法在返回张量形状变化的示例对比。\n\n```bash\n>>> x = torch.Tensor([ # shape is [2, 5]\n...     [2,3,4,5,6],\n...     [9,8,7,6,5,]\n... ])\n\n>>> print(torch.cumprod(x, dim = 0))\ntensor([[ 2.,  3.,  4.,  5.,  6.],\n        [18., 24., 28., 30., 30.]])\n\n>>> print(torch.cumprod(x, dim = 1)) # output shape is [2, 5]\ntensor([[2.0000e+00, 6.0000e+00, 2.4000e+01, 1.2000e+02, 7.2000e+02],\n        [9.0000e+00, 7.2000e+01, 5.0400e+02, 3.0240e+03, 1.5120e+04]])\n\n>>> torch.min(x, dim = 0) # output shape is [5]\ntorch.return_types.min(\nvalues=tensor([2., 3., 4., 5., 5.]),\nindices=tensor([0, 0, 0, 0, 1]))\n\n>>> print(torch.max(x, dim = 1)) # output shape is [2]\ntorch.return_types.max(\nvalues=tensor([6., 9.]),\nindices=tensor([4, 0]))\n\n>>> torch.mean(x, dim = 0, keepdim = True) # output shape is [1, 5]\ntensor([[5.5000, 5.5000, 5.5000, 5.5000, 5.5000]]) \n```"
  },
  {
    "path": "2-deep_learning_basic/pytorch_basic/Pytorch基础-张量结构操作.md",
    "content": "- [一，张量的基本操作](#一张量的基本操作)\n  - [1.1 改变形状: view 和 reshape](#11-改变形状-view-和-reshape)\n  - [1.2 张量拼接](#12-张量拼接)\n- [二，维度变换](#二维度变换)\n  - [2.1 unsqueeze vs squeeze 维度增减](#21-unsqueeze-vs-squeeze-维度增减)\n  - [2.2，transpose vs permute 维度交换](#22transpose-vs-permute-维度交换)\n- [三 索引切片](#三-索引切片)\n  - [3.1 规则索引切片方式](#31-规则索引切片方式)\n  - [3.2 gather 和 torch.index\\_select 算子](#32-gather-和-torchindex_select-算子)\n- [四，合并分割](#四合并分割)\n  - [4.1，torch.cat 和 torch.stack](#41torchcat-和-torchstack)\n  - [4.2 torch.split 和  torch.chunk](#42-torchsplit-和--torchchunk)\n- [五 卷积相关算子](#五-卷积相关算子)\n  - [5.1 上采样方法总结](#51-上采样方法总结)\n  - [5.2，F.interpolate 采样函数](#52finterpolate-采样函数)\n  - [5.3 nn.ConvTranspose2d 反卷积](#53-nnconvtranspose2d-反卷积)\n- [参考资料](#参考资料)\n\n> 授人以鱼不如授人以渔，原汁原味的知识才更富有精华，本文只是对张量基本操作知识的理解和学习笔记，看完之后，想要更深入理解，建议去 pytorch 官方网站，查阅相关函数和操作，英文版在[这里](https://pytorch.org/docs/1.7.0/torch.html)，中文版在[这里](https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch/#tensors)。本文的代码是在 `pytorch1.7` 版本上测试的，其他版本一般也没问题。\n\n## 一，张量的基本操作\n`Pytorch` 中，张量的操作分为**结构操作和数学运算**，其理解就如字面意思。结构操作就是改变张量本身的结构，数学运算就是对张量的元素值完成数学运算。\n+ 常使用的张量结构操作：维度变换（`tranpose`、`view` 等）、合并分割（`split`、`chunk`等）、索引切片（`index_select`、`gather` 等）。\n+ 常用的张量数学运算：标量运算、向量运算、矩阵运算。\n\n### 1.1 改变形状: view 和 reshape\n\n两个方法都是用来改变 tensor 的 shape，**view() 只适合对满足连续性条件（`contiguous`）的 tensor 进行操作**，而 reshape() 同时还可以对不满足连续性条件的 tensor 进行操作。在满足 tensor 连续性条件（`contiguous`）时，a.reshape()  返回的结果与 a.view() 相同，都不会开辟新内存空间；不满足 `contiguous` 时， 直接使用 view() 方法会失败，`reshape()` 依然有用，但是会重新开辟内存空间，不与之前的 tensor 共享内存，即返回的是 **”副本“**（等价于先调用 `contiguous()` 方法再使用 `view()` 方法）。\n\n`view` 和 `reshape` 的区别总结如下表所示:\n\n| 特性   | view                                           | reshape                                      |\n|--------|-----------------------------------------------|----------------------------------------------|\n| **依赖存储布局** | 需要张量的内存是连续的。如果张量内存不连续，会报错。 | 不依赖内存是否连续，会创建新的内存副本（如果必要）。 |\n| **性能**     | 更快，因为它只是在内存上重新定义视图，不移动数据。      | 更灵活，但可能会牺牲一些性能。                     |\n| **语义**     | 是对原张量的视图，修改新张量会影响原张量的数据。         | 可能创建新张量，原张量不会受到影响。               |\n\n> 更多理解可以参考这篇[文章](https://blog.csdn.net/Flag_ing/article/details/109129752)。\n\n### 1.2 张量拼接\n\n`torch.concat`（别名 torch.cat）用于沿着指定维度拼接张量的操作。语法：`torch.cat(tensors, dim=0)`。\n\n- `tensors`：一个包含待拼接张量的序列（如列表或元组）。\n- `dim`：指定沿哪一个维度进行拼接。\n\n示例代码\n\n```bash\n>>> import torch\n>>> A = torch.tensor([[1, 2], [3, 4]]) # 形状 [2, 2]\n>>> B = torch.tensor([[5, 6], [7, 8]]) # 形状 [2, 2]\n>>> C = torch.concat([A, B], dim = 0)\n>>> C\ntensor([[1, 2],\n        [3, 4],\n        [5, 6],\n        [7, 8]])\n>>> C2 = torch.concat([A, B], dim = 1)\n>>> C2\ntensor([[1, 2, 5, 6],\n        [3, 4, 7, 8]])\n```\n\n## 二，维度变换\n\n### 2.1 unsqueeze vs squeeze 维度增减\n\n- `torch.squeeze`: 默认用于移除张量中所有大小为 1 的维度。\n- `torch.unsqueeze`: 用于在指定位置插入一个大小为 1 的维度，从而增加张量的维度。\n\n```python\n# 参数, input：输入张量。dim（可选）：指定要移除的维度。如果未指定，则移除所有大小为 1 的维度。\ntorch.squeeze(input, dim=None)\n# 参数, input：输入张量。dim：指定插入维度的位置。\ntorch.unsqueeze(input, dim)\n```\n\n> “Squeeze” 单词作动词时，主要意思是“挤压、捏、榨取”\n\n`squeeze` 用例程序如下：\n```python\na = torch.rand(1,1,3,3)\nb = torch.squeeze(a)\nc = a.squeeze(1)\nprint(b.shape)\nprint(c.shape)\n```\n程序输出结果如下：\n> torch.Size([3, 3])\ntorch.Size([1, 3, 3])\n\n`unsqueeze` 用例程序如下：\n```python\nx = torch.rand(3,3)\ny1 = torch.unsqueeze(x, 0)\ny2 = x.unsqueeze(0)\nprint(y1.shape)\nprint(y2.shape)\n```\n程序输出结果如下：\n> torch.Size([1, 3, 3])\ntorch.Size([1, 3, 3])\n\n### 2.2，transpose vs permute 维度交换\n`torch.transpose()` 只能交换两个维度，而 `.permute()` 可以自由交换任意位置。函数定义如下：\n```python\ntranspose(dim0, dim1) → Tensor  # See torch.transpose()\npermute(*dims) → Tensor  # dim(int). Returns a view of the original tensor with its dimensions permuted.\n```\n在 `CNN` 模型中，我们经常遇到交换维度的问题，举例：四个维度表示的 tensor：`[batch, channel, h, w]`（`nchw`），如果想把 `channel` 放到最后去，形成`[batch, h, w, channel]`（`nhwc`），如果使用 `torch.transpose()` 方法，至少要交换两次（先 `1 3` 交换再 `1 2` 交换），而使用 `.permute()` 方法只需一次操作，更加方便。例子程序如下：\n```python\nimport torch\ninput = torch.rand(1,3,28,32)                    # torch.Size([1, 3, 28, 32]\nprint(b.transpose(1, 3).shape)                   # torch.Size([1, 32, 28, 3])\nprint(b.transpose(1, 3).transpose(1, 2).shape)   # torch.Size([1, 28, 32, 3])\n \nprint(b.permute(0,2,3,1).shape)                  # torch.Size([1, 28, 28, 3]\n```\n## 三 索引切片\n\n### 3.1 规则索引切片方式\n\n张量的索引切片方式和 `numpy`、python 多维列表几乎一致，都可以通过索引和切片对部分元素进行修改。切片时支持缺省参数和省略号。实例代码如下：\n```python\n>>> t = torch.randint(1,10,[3,3])\n>>> t\ntensor([[8, 2, 9],\n        [2, 5, 9],\n        [3, 9, 9]])\n>>> t[0] # 第 1 行数据\ntensor([8, 2, 9])\n>>> t[2][2]\ntensor(9)\n>>> t[0:3,:]  # 第1至第3行，全部列\ntensor([[8, 2, 9],\n        [2, 5, 9],\n        [3, 9, 9]])\n>>> t[0:2,:]  # 第1行至第2行\ntensor([[8, 2, 9],\n        [2, 5, 9]])\n>>> t[1:,-1]  # 第2行至最后行，最后一列\ntensor([9, 9])\n>>> t[1:,::2] # 第1行至最后行，第0列到最后一列每隔两列取一列\ntensor([[2, 9],\n        [3, 9]])\n```\n以上切片方式相对规则，对于不规则的切片提取,可以使用 `torch.index_select`, `torch.take`, `torch.gather`, `torch.masked_select`。\n\n### 3.2 gather 和 torch.index_select 算子\n> `gather` 算子的用法比较难以理解，在翻阅了官方文档和网上资料后，我有了一些自己的理解。\n\n1，`gather` 是不规则的切片提取算子（Gathers values along an axis specified by dim. 在指定维度上根据索引 index 来选取数据）。函数定义如下：\n```python\ntorch.gather(input, dim, index, *, sparse_grad=False, out=None) → Tensor\n```\n**参数解释**：\n+ `input` (Tensor) – the source tensor.\n+ `dim` (int) – the axis along which to index.\n+ `index` (LongTensor) – the indices of elements to gather.\n\n`gather` 算子的注意事项：\n+ 输入 `input` 和索引 `index` 具有相同数量的维度，即 `input.shape = index.shape`\n+ 对于任意维数，只要 `d != dim`，index.size(d) <= input.size(d)，即对于可以不用索引维数 `d` 上的全部数据。\n+ 输出 `out` 和 索引 `index` 具有相同的形状。输入和索引不会相互广播。\n\n对于 3D tensor，`output` 值的定义如下：\n`gather` 的官方定义如下：\n```python\nout[i][j][k] = input[index[i][j][k]][j][k]  # if dim == 0\nout[i][j][k] = input[i][index[i][j][k]][k]  # if dim == 1\nout[i][j][k] = input[i][j][index[i][j][k]]   # if dim == 2\n```\n通过理解前面的一些定义，相信读者对 `gather` 算子的用法有了一个基本了解，下面再结合 2D 和 3D  tensor 的用例来直观理解算子用法。\n（1），对于 2D tensor 的例子：\n```python\n>>> import torch\n>>> a = torch.arange(0, 16).view(4,4)\n>>> a\ntensor([[ 0,  1,  2,  3],\n        [ 4,  5,  6,  7],\n        [ 8,  9, 10, 11],\n        [12, 13, 14, 15]])\n>>> index = torch.tensor([[0, 1, 2, 3]])  # 选取对角线元素\n>>> torch.gather(a, 0, index)\ntensor([[ 0,  5, 10, 15]])\n```\n`output` 值定义如下：\n```shell\n# 按照 index = tensor([[0, 1, 2, 3]])顺序作用在行上索引依次为0,1,2,3\na[0][0] = 0\na[1][1] = 5\na[2][2] = 10\na[3][3] = 15\n```\n（2），索引更复杂的  2D tensor 例子：\n```python\n>>> t = torch.tensor([[1, 2], [3, 4]])\n>>> t\ntensor([[1, 2],\n        [3, 4]])\n>>> torch.gather(t, 1, torch.tensor([[0, 0], [1, 0]]))\ntensor([[ 1,  1],\n        [ 4,  3]])\n```\n\n`output` 值的计算如下：\n\n```shell\noutput[i][j] = input[i][index[i][j]]  # if dim = 1\noutput[0][0] = input[0][index[0][0]] = input[0][0] = 1\noutput[0][1] = input[0][index[0][1]] = input[0][0] = 1\noutput[1][0] = input[1][index[1][0]] = input[1][1] = 4\noutput[1][1] = input[1][index[1][1]] = input[1][0] = 3\n```\n\n总结：**可以看到 `gather` 是通过将索引在指定维度 `dim` 上的值替换为 `index` 的值，但是其他维度索引不变的情况下获取 `tensor` 数据**。直观上可以理解为对矩阵进行重排，比如对每一行(dim=1)的元素进行变换，比如 `torch.gather(a, 1, torch.tensor([[1,2,0], [1,2,0]]))` 的作用就是对 矩阵 `a` 每一行的元素，进行 `permtute(1,2,0)` 操作。\n\n2，理解了 `gather` 再看 `index_select` 就很简单，函数作用是返回沿着输入张量的指定维度的指定索引号进行索引的张量子集。函数定义如下：\n\n```python\ntorch.index_select(input, dim, index, *, out=None) → Tensor\n```\n\n函数返回一个新的张量，它使用数据类型为 `LongTensor` 的 `index` 中的条目沿维度 `dim` 索引输入张量。返回的张量具有与原始张量（输入）相同的维数。 维度尺寸与索引长度相同； 其他尺寸与原始张量中的尺寸相同。实例代码如下：\n\n```python\n>>> x = torch.randn(3, 4)\n>>> x\ntensor([[ 0.1427,  0.0231, -0.5414, -1.0009],\n        [-0.4664,  0.2647, -0.1228, -1.1068],\n        [-1.1734, -0.6571,  0.7230, -0.6004]])\n>>> indices = torch.tensor([0, 2])\n>>> torch.index_select(x, 0, indices)\ntensor([[ 0.1427,  0.0231, -0.5414, -1.0009],\n        [-1.1734, -0.6571,  0.7230, -0.6004]])\n>>> torch.index_select(x, 1, indices)\ntensor([[ 0.1427, -0.5414],\n        [-0.4664, -0.1228],\n        [-1.1734,  0.7230]])\n```\n## 四，合并分割\n\n### 4.1，torch.cat 和 torch.stack\n\n可以用 `torch.cat` 方法和 `torch.stack` 方法将多个张量合并，也可以用 `torch.split`方法把一个张量分割成多个张量。`torch.cat` 和 `torch.stack` 有略微的区别，`torch.cat` 是连接，不会增加维度，而 `torch.stack` 是堆叠，会增加一个维度。两者函数定义如下：\n```python\n# 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.\ntorch.cat(tensors, dim=0, *, out=None) → Tensor\n# Concatenates a sequence of tensors along **a new** dimension. All tensors need to be of the same size.\ntorch.stack(tensors, dim=0, *, out=None) → Tensor\n```\n`torch.cat` 和 `torch.stack` 用法实例代码如下：\n```python\n>>> a = torch.arange(0,9).view(3,3)\n>>> b = torch.arange(10,19).view(3,3)\n>>> c = torch.arange(20,29).view(3,3)\n>>> cat_abc = torch.cat([a,b,c], dim=0)\n>>> print(cat_abc.shape)\ntorch.Size([9, 3])\n>>> print(cat_abc)\ntensor([[ 0,  1,  2],\n        [ 3,  4,  5],\n        [ 6,  7,  8],\n        [10, 11, 12],\n        [13, 14, 15],\n        [16, 17, 18],\n        [20, 21, 22],\n        [23, 24, 25],\n        [26, 27, 28]])\n>>> stack_abc = torch.stack([a,b,c], axis=0)  # torch中dim和axis参数名可以混用\n>>> print(stack_abc.shape)\ntorch.Size([3, 3, 3])\n>>> print(stack_abc)\ntensor([[[ 0,  1,  2],\n         [ 3,  4,  5],\n         [ 6,  7,  8]],\n\n        [[10, 11, 12],\n         [13, 14, 15],\n         [16, 17, 18]],\n\n        [[20, 21, 22],\n         [23, 24, 25],\n         [26, 27, 28]]])\n>>> chunk_abc = torch.chunk(cat_abc, 3, dim=0)\n>>> chunk_abc\n(tensor([[0, 1, 2],\n         [3, 4, 5],\n         [6, 7, 8]]),\n tensor([[10, 11, 12],\n         [13, 14, 15],\n         [16, 17, 18]]),\n tensor([[20, 21, 22],\n         [23, 24, 25],\n         [26, 27, 28]]))\n```\n\n另外，`torch.hstack` 函数用于将一系列张量沿水平方向（列方向）堆叠。对于一维张量，相当于直接按元素连接；对于二维及更高维张量，相当于沿第二个维度（列维度）堆叠。它是 torch.cat(tensors, dim=1) 的简便形式。\n\n两个二维张量的 `hstack` 操作过程可视化如下图所示:\n\n![hstack_visual](../../images/pytorch_tensor_structure/hstack_visual.png)\n\n### 4.2 torch.split 和  torch.chunk\n\n`torch.split()` 和 `torch.chunk()` 可以看作是 `torch.cat()` 的逆运算。`split()` 作用是将张量拆分为多个块，每个块都是原始张量的视图。`split()` [函数定义](https://pytorch.org/docs/stable/generated/torch.split.html#torch.split)如下：\n```python\n\"\"\"\nSplits the tensor into chunks. Each chunk is a view of the original tensor.\nIf 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.\nIf 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.\n\"\"\"\ntorch.split(tensor, split_size_or_sections, dim=0)\n```\n`chunk()` 作用是将 `tensor` 按 `dim`（行或列）分割成 `chunks` 个 `tensor` 块，返回的是一个元组。`chunk()` 函数定义如下：\n```python\ntorch.chunk(input, chunks, dim=0) → List of Tensors\n\"\"\"\nSplits a tensor into a specific number of chunks. Each chunk is a view of the input tensor.\nLast chunk will be smaller if the tensor size along the given dimension dim is not divisible by chunks.\nParameters:\n    input (Tensor) – the tensor to split\n    chunks (int) – number of chunks to return\n    dim (int) – dimension along which to split the tensor\n\"\"\"\n```\n实例代码如下：\n```python\n>>> a = torch.arange(10).reshape(5,2)\n>>> a\ntensor([[0, 1],\n        [2, 3],\n        [4, 5],\n        [6, 7],\n        [8, 9]])\n>>> torch.split(a, 2)\n(tensor([[0, 1],\n         [2, 3]]),\n tensor([[4, 5],\n         [6, 7]]),\n tensor([[8, 9]]))\n>>> torch.split(a, [1,4])\n(tensor([[0, 1]]),\n tensor([[2, 3],\n         [4, 5],\n         [6, 7],\n         [8, 9]]))\n>>> torch.chunk(a, 2, dim=1)\n(tensor([[0],\n        [2],\n        [4],\n        [6],\n        [8]]), \ntensor([[1],\n        [3],\n        [5],\n        [7],\n        [9]]))\n```\n\n## 五 卷积相关算子\n### 5.1 上采样方法总结\n上采样大致被总结成了三个类别：\n1. 基于线性插值的上采样：最近邻算法（`nearest`）、双线性插值算法（`bilinear`）、双三次插值算法（`bicubic`）等，这是传统图像处理方法。\n2. 基于深度学习的上采样（转置卷积，也叫反卷积 `Conv2dTranspose2d`等）\n3. `Unpooling` 的方法（简单的补零或者扩充操作）\n\n> 计算效果：最近邻插值算法 < 双线性插值 < 双三次插值。计算速度：最近邻插值算法 > 双线性插值 > 双三次插值。\n### 5.2，F.interpolate 采样函数\n> Pytorch 老版本有 `nn.Upsample` 函数，新版本建议用 `torch.nn.functional.interpolate`，一个函数可实现定制化需求的上采样或者下采样功能，。\n\n`F.interpolate()` 函数全称是 `torch.nn.functional.interpolate()`，函数定义如下：\n```python\ndef interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None):  # noqa: F811\n    # type: (Tensor, Optional[int], Optional[List[float]], str, Optional[bool], Optional[bool]) -> Tensor\n    pass\n```\n参数解释如下：\n+ `input`(Tensor)：输入张量数据；\n+ `size`： 输出的尺寸，数据类型为 tuple： ([optional D_out], [optional H_out], W_out)，和 `scale_factor` 二选一。\n+ `scale_factor`：在高度、宽度和深度上面的放大倍数。数据类型既可以是 int`——表明高度、宽度、深度都扩大同一倍数；也可是 `tuple`——指定高度、宽度、深度等维度的扩大倍数。\n+ `mode`： 上采样的方法，包括最近邻（`nearest`），线性插值（`linear`），双线性插值（`bilinear`），三次线性插值（`trilinear`），默认是最近邻（`nearest`）。\n+ `align_corners`： 如果设为 `True`，输入图像和输出图像角点的像素将会被对齐（aligned），这只在 `mode = linear, bilinear, or trilinear` 才有效，默认为 `False`。\n\n例子程序如下：\n```python\nimport torch.nn.functional as F\nx = torch.rand(1,3,224,224)\ny = F.interpolate(x * 2, scale_factor=(2, 2), mode='bilinear').squeeze(0)\nprint(y.shape)   # torch.Size([3, 224, 224)\n```\n\n### 5.3 nn.ConvTranspose2d 反卷积\n\n转置卷积（有时候也称为反卷积，个人觉得这种叫法不是很规范），它是一种特殊的卷积，先 `padding` 来扩大图像尺寸，紧接着跟正向卷积一样，旋转卷积核 180 度，再进行卷积计算。\n\n【等待更新】\n\n## 参考资料\n\n+ [pytorch演示卷积和反卷积运算](https://blog.csdn.net/qq_37879432/article/details/80297263)\n+ [torch.Tensor](https://pytorch.org/docs/1.7.0/tensors.html#torch.Tensor)\n+ [PyTorch学习笔记(10)——上采样和PixelShuffle](https://blog.csdn.net/g11d111/article/details/82855946)\n+ [反卷积 Transposed convolution](https://zhuanlan.zhihu.com/p/124626648)\n+ [PyTorch中的转置卷积详解——全网最细](https://blog.csdn.net/w55100/article/details/106467776)\n+ [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)"
  },
  {
    "path": "2-deep_learning_basic/pytorch_basic/src/tensor_demo.py",
    "content": "import torch\n\ndef tensor_learn():\n    matrix = torch.tensor([[[1,2,3,4],[5,6,7,8]],\n                       [[5,4,6,7], [5,6,8,9]]], dtype = torch.float64)\n    print(matrix)               # 打印 tensor\n    print(matrix.dtype)     # 打印 tensor 数据类型\n    print(matrix.dim())     # 打印 tensor 维度\n    print(matrix.size())     # 打印 tensor 尺寸\n    print(matrix.shape)    # 打印 tensor 尺寸\n    matrix2 = matrix.view(4, 2, 2) # 改变 tensor 尺寸\n    print(matrix2)\n    print(matrix.numel())\n    \ndef generate_tensor_and_print_stats(size, func=\"normal\", mean=0.0, std=1.0):\n    \"\"\"\n    生成一个张量并打印其均值和标准差。\n\n    参数:\n    - size (tuple): 生成张量的形状。\n    - func (str): 使用的生成函数。选项包括 \"normal\" 和 \"randn\"。\n    - mean (float, 可选): 正态分布的均值，仅在 func=\"normal\" 时使用。默认值为0.0。\n    - std (float, 可选): 正态分布的标准差，仅在 func=\"normal\" 时使用。默认值为1.0。\n\n    返回:\n    - tensor (Tensor): 生成的张量。\n    \"\"\"\n    if func == \"normal\":\n        tensor = torch.normal(mean=mean, std=std, size=size)\n    elif func == \"randn\":\n        tensor = torch.randn(size=size)\n    else:\n        raise ValueError(f\"无效的函数类型: {func}. 请使用 'normal' 或 'randn'。\")\n    \n    tensor_mean = tensor.mean().item()\n    tensor_std = tensor.std().item()\n    \n    print(f\"Tensor Size: {size}, by torch.{func}, Mean: {tensor_mean:.4f}, Std Dev: {tensor_std:.4f}\")\n    return tensor\n\ndef main():\n    tensor_learn()\n    # 设置随机种子以确保结果可重复（可选）\n    torch.manual_seed(40)\n\n    # 定义不同的张量大小\n    sizes = [(3, 3), (100, 100), (1000, 1000)]\n    functions = [\"normal\", \"randn\"]\n\n    # 生成并打印统计信息\n    for func in functions:\n        for size in sizes:\n            if func == \"normal\":\n                generate_tensor_and_print_stats(size=size, func=func, mean=0.0, std=1.0)\n            elif func == \"randn\":\n                generate_tensor_and_print_stats(size=size, func=func)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "2-deep_learning_basic/pytorch_basic/src/tensor_math.py",
    "content": "import torch\n\n# 创建一个 2D 张量\nb = torch.tensor([[1, 2, 3], [4, 5, 6]])\nprint(\"原始张量：\\n\", b)\n\n# 沿第 0 维计算累积乘积\ncumprod_b_dim0 = torch.cumprod(b, dim=0)\nprint(\"沿第 0 维的累积乘积结果：\\n\", cumprod_b_dim0)\n\n# 沿第 1 维计算累积乘积\ncumprod_b_dim1 = torch.cumprod(b, dim=1)\nprint(\"沿第 1 维的累积乘积结果：\\n\", cumprod_b_dim1)"
  },
  {
    "path": "2-deep_learning_basic/pytorch_code/pytorch-c10模块详解.md",
    "content": "- [一 c10 模块概述](#一-c10-模块概述)\n- [二 Stream 类](#二-stream-类)\n  - [2.1 Stream 抽象类](#21-stream-抽象类)\n  - [2.2 CUDAStream 类](#22-cudastream-类)\n  - [2.2 Stream python 类](#22-stream-python-类)\n- [三 Event 类](#三-event-类)\n  - [3.1 Event 类](#31-event-类)\n- [四 设备管理工具类-InlineDeviceGuard](#四-设备管理工具类-inlinedeviceguard)\n- [参考资料](#参考资料)\n\n## 一 c10 模块概述\n\n`c10` 模块更倾向于提供**高级别的抽象和功能**，而 `ATen`则提供了**更接近硬件的底层操作**。c10 主要模块包括：\n\n+ **c10/core**: 该子目录定义了很多基础类型和核心概念，如 **Device**、**Stream**、**DataPtr**、错误处理（例如 C10_ERROR 系列接口）等。\n    - `TensorImpl`：Tensor 的底层实现，存储数据指针、形状（sizes）、步长（strides）、设备（device）等元信息。\n    - `ScalarType`：定义**标量**数据类型（如 kFloat、kInt）。\n    - **Device**：封装了设备信息（如 CPU、CUDA 等），支持设备间的统一管理。\n    - **Stream**：提供对异步流（如 CUDA 流）的抽象，允许在不同设备上执行并行操作。\n    - 其他核心组件还可能包括与内存管理、后端调度相关的工具。\n+ c10/cuda/：作用：CUDA 设备管理、流（Stream）、事件（Event）、内存分配等。\n    - CUDAStream：定义 CUDA 流（cudaStream_t）抽象接口，管理异步 GPU 操作。\n    - CUDAGuard：设置当前设备（Device）和流（Stream）的上下文守卫。\n    - CUDAFunctions：CUDA 运行时 API 的封装（如设备同步、内存拷贝）。\n    - CUDACachingAllocator：CUDA 内存分配器，支持高效的内存池管理。\n+ **c10/util**: 包含各种实用的模板和辅助工具，例如 **Optional**、**ArrayRef**、类型特性工具等，这些工具在整个框架中帮助简化数据结构管理和算法实现。\n+ **c10/macros**: 提供了大量宏定义，用于统一错误检查、分支预测优化（如`C10_LIKELY/C10_UNLIKELY`）、调试信息输出、编译器平台适配等功能。这些宏在高性能代码路径中起到优化和代码风格统一的作用。\n+ **其他模块**:根据版本不同，c10 中可能还会包含与特定后端（例如 `CUDA`、`hip` 相关）的适配代码、日志系统实现、以及与分布式或异步执行有关的工具。\n\n## 二 Stream 类\n\n### 2.1 Stream 抽象类\n\n`Stream` 主要用于表示一个设备（通常是 GPU，比如 CUDA）上的操作队列或执行流。pytorch 中 c10 模块的 `Stream` 抽象定义的接口实现在 `c10/core/Stream.h`文件中。\n\n`Stream` 抽象类定义的公有成员函数如下所示：\n\n<center>\n<img src=\"../../images/pytorch_c10/stream_funcs.png\" width=\"20%\" alt=\"stream_funcs\">\n</center>\n\n`Stream` 类定义代码如下所示:\n\n```cpp\nusing StreamId = int64_t;\n\nclass C10_API Stream final {\n private:\n  Device device_;\n  StreamId id_;\n\n public:\n  enum Unsafe { UNSAFE };\n  enum Default { DEFAULT };\n\n  /// Unsafely construct a stream from a Device and a StreamId.  In\n  /// general, only specific implementations of streams for a\n  /// backend should manufacture Stream directly in this way; other users\n  /// should use the provided APIs to get a stream.  In particular,\n  /// we don't require backends to give any guarantees about non-zero\n  /// StreamIds; they are welcome to allocate in whatever way they like.\n  explicit Stream(Unsafe, Device device, StreamId id)\n      : device_(device), id_(id) {}\n\n  /// Construct the default stream of a Device.  The default stream is\n  /// NOT the same as the current stream; default stream is a fixed stream\n  /// that never changes, whereas the current stream may be changed by\n  /// StreamGuard.\n  explicit Stream(Default, Device device) : device_(device), id_(0) {}\n\n  bool operator==(const Stream& other) const noexcept {\n    return this->device_ == other.device_ && this->id_ == other.id_;\n  }\n  bool operator!=(const Stream& other) const noexcept {\n    return !(*this == other);\n  }\n\n  Device device() const noexcept {\n    return device_;\n  }\n  DeviceType device_type() const noexcept {\n    return device_.type();\n  }\n  DeviceIndex device_index() const noexcept {\n    return device_.index();\n  }\n  StreamId id() const noexcept {\n    return id_;\n  }\n\n  // Enqueues a wait instruction in the stream's work queue.\n  // This instruction is a no-op unless the event is marked\n  // for recording. In that case the stream stops processing\n  // until the event is recorded.\n  template <typename T>\n  void wait(const T& event) const {\n    event.block(*this);\n  }\n\n  // Return whether all asynchronous work previously enqueued on this stream\n  // has completed running on the device.\n  bool query() const;\n\n  // Wait (by blocking the calling thread) until all asynchronous work enqueued\n  // on this stream has completed running on the device.\n  void synchronize() const;\n\n  // The purpose of this function is to more conveniently permit binding\n  // of Stream to and from Python.  Without packing, I have to setup a whole\n  // class with two fields (device and stream id); with packing I can just\n  // store a single uint64_t.\n  //\n  // The particular way we pack streams into a uint64_t is considered an\n  // implementation detail and should not be relied upon.\n  uint64_t hash() const noexcept {\n    // Concat these together into a 64-bit integer\n    uint64_t bits = static_cast<uint64_t>(device_type()) << 56 |\n        static_cast<uint64_t>(device_index()) << 48 |\n        // Remove the sign extension part of the 64-bit address because\n        // the id might be used to hold a pointer.\n        (static_cast<uint64_t>(id()) & ((1ull << 48) - 1));\n    return bits;\n  }\n\n  struct StreamData3 pack3() const {\n    return {id(), device_index(), device_type()};\n  }\n\n  static Stream unpack3(\n      StreamId stream_id,\n      DeviceIndex device_index,\n      DeviceType device_type) {\n    TORCH_CHECK(isValidDeviceType(device_type));\n    return Stream(UNSAFE, Device(device_type, device_index), stream_id);\n  }\n\n  // I decided NOT to provide setters on this class, because really,\n  // why would you change the device of a stream?  Just construct\n  // it correctly from the beginning dude.\n};\n\nC10_API std::ostream& operator<<(std::ostream& stream, const Stream& s);\n\n} // namespace c10\n```\n\n### 2.2 CUDAStream 类\n\nCuda API 可分为同步和异步两类，**同步函数会阻塞 host 端的线程执行，异步函数会立刻将控制权返还给 host 从而继续执行之后的动作。**异步函数和 stream 是 grid level 并行的两个基石\n\nCUDAStream 是 PyTorch 管理 CUDA 异步操作的核心类，用于控制 GPU 任务的并行执行和同步。Cuda stream 对象是指一堆异步的 cuda 操作，他们按照 host 代码调用的顺序执行在 device上。Stream 维护了这些操作的顺序，并在所有预处理完成后允许这些操作进入工作队列，同时也可以对这些操作进行一些查询操作。\n\nCUDAStream 类的代码实现在 `c10/cuda/CUDAStream.h` 和 `CUDAStream.cpp` 文件中。私有成员变量定义：\n\n```cpp\nclass C10_CUDA_API CUDAStream {\n public:\n  enum Unchecked { UNCHECKED };\n\n  /// Construct a CUDAStream from a Stream.  This construction is checked,\n  /// and will raise an error if the Stream is not, in fact, a CUDA stream.\n  explicit CUDAStream(Stream stream) : stream_(stream) {\n    TORCH_CHECK(stream_.device_type() == DeviceType::CUDA);\n  }\n  // 代码省略\n  private:\n    Stream stream_; // 公共抽象父类对象，定义在 c10/core/Stream.h\n}\n```\n\n2, 公有成员函数。\n\n<center>\n<img src=\"../../images/pytorch_c10/cuda_stream_funcs.png\" width=\"20%\" alt=\"cuda_stream_funcs\">\n</center>\n\n3，`CUDAStream` 流的创建和销毁。\n\n- 默认流：\n    - 每个设备有一个默认流（Default Stream），通过 `getDefaultCUDAStream` 函数获取默认流对象。\n    - 默认流是同步流，操作按顺序执行。\n\n```cpp\n/**\n * Get the default CUDA stream, for the passed CUDA device, or for the\n * current device if no device index is passed.  The default stream is\n * where most computation occurs when you aren't explicitly using\n * streams.\n */\nC10_API CUDAStream getDefaultCUDAStream(DeviceIndex device_index = -1);\n```\n\n- **独立流**：\n    - 通过 `getStreamFromPool` 从流池中获取，避免频繁创建销毁流的开销。\n    - 独立流可以并行执行，但需要手动同步。\n\n```cpp\n/**\n * Get a new stream from the CUDA stream pool.  You can think of this\n * as \"creating\" a new stream, but no such creation actually happens;\n * instead, streams are preallocated from the pool and returned in a\n * round-robin fashion.\n *\n * You can request a stream from the high priority pool by setting\n * isHighPriority to true, or a stream for a specific device by setting device\n * (defaulting to the current CUDA stream.)\n */\nC10_API CUDAStream\ngetStreamFromPool(const bool isHighPriority = false, DeviceIndex device = -1);\n// no default priority to disambiguate overloads\nC10_API CUDAStream\ngetStreamFromPool(const int priority, DeviceIndex device = -1);\n```\n\n4，各个函数实现分析。\n\n```cpp\n// 目的：确保当前流的所有 CUDA 操作执行完毕，提供一种同步机制。\nvoid synchronize() const {\n    // 确保在调用 CUDA 同步操作前，将当前设备切换为流所属的设备\n    DeviceGuard guard{stream_.device()};\n    // 调用 c10::cuda::stream_synchronize() 对当前流进行同步，\n    // 即阻塞直到流中所有操作执行完毕，确保 CUDA 操作已完成\n    c10::cuda::stream_synchronize(stream());\n}\n```\n\n### 2.2 Stream python 类\n\n`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。\n\n+ `Stream` 类包含 wait_event、wait_stream、record_event、 query 和 synchronize 等成员函数。\n+ `Event` 类主要包含：record、wait、query、elapsed_time 和 synchronize 等成员函数。\n\n<center>\n<img src=\"../../images/pytorch_c10/cuda_stream_py_funcs.png\" width=\"20%\" alt=\"cuda_stream_py_funcs\">\n</center>\n\n## 三 Event 类\n\n### 3.1 Event 类\n\n1，什么是 Event\n\nEvent 是 stream 相关的一个重要设计，用于在 CUDA Stream 中插入事件，**记录 GPU 操作的时间戳或实现流间同步**。`Event` 类的关键方法：\n- `record(stream=None)`：在指定流中记录事件。\n- `synchronize()`：阻塞 CPU 主机线程，直到直到此事件中当前捕获的所有工作完成。\n- `wait(stream)`：让提交到给定 stream 的所有未来工作等待此事件。如果未指定流，则使用 `torch.cuda.current_stream()`。\n- `elapsed_time(end_event)`：计算两个事件的时间差（毫秒）。\n- `query()`: 提供了非阻塞式的完成状态检查机制。\n\n2，Event 的生命周期\n\n- **创建**（cudaEventCreate）：调用 cudaEventCreate() 或 cudaEventCreateWithFlags() 创建一个 Event 对象。\n- **记录**（cudaEventRecord）：将 Event 推入指定的 stream。如果你使用默认流（即 0 或 cudaStreamDefault），则该 Event 会在所有流上顺序生效（见下文默认流语义）。\n- **查询**（cudaEventQuery）：异步查询该 Event 是否已完成。如果为 “未完成”，函数会立即返回 cudaErrorNotReady；只有当该 Event 标记的 stream 中位于它之前的所有操作都执行完毕，query() 才会返回 cudaSuccess。\n- 等待/同步（cudaEventSynchronize）：阻塞当前 CPU 线程，直到该 Event 完成。\n- 销毁（cudaEventDestroy）：释放 Event 资源。\n\n下图是是 c10/core/Event.h 中 Event 类的成员函数总结，各个函数的实现其实是在 c10/core/impl/InlineEvent.h 中。\n\n<center>\n<img src=\"../../images/pytorch_c10/event_funcs.png\" width=\"20%\" alt=\"event_funcs\">\n</center>\n\n`c10/core/impl/InlineEvent.h` 文件中 `InlineEvent` 类的总结和注释如下所示:\n\n```cpp\n// InlineEvent 是一个模板结构体，用于封装事件（Event）的操作，其行为依赖于具体的后端实现（通过模板参数 T 提供）。\n// 注意：构造函数被删除，必须传入设备类型和可选的 EventFlag 进行初始化。\ntemplate <typename T>\nstruct InlineEvent final {\n  // 禁用默认构造函数\n  InlineEvent() = delete;\n\n  // 构造函数：需要传入设备类型和可选的标志参数（默认为 PYTORCH_DEFAULT）\n  InlineEvent(\n      const DeviceType _device_type,\n      const EventFlag _flag = EventFlag::PYTORCH_DEFAULT)\n      : backend_{_device_type},            // 利用设备类型初始化后端实现对象\n        device_type_{_device_type},         // 记录事件所属的设备类型\n        flag_{_flag} {}                     // 记录事件标志\n\n  // 禁用拷贝构造和拷贝赋值，避免误拷贝事件对象\n  InlineEvent(const InlineEvent&) = delete;\n  InlineEvent& operator=(const InlineEvent&) = delete;\n\n  // 移动构造函数，允许事件对象在移动语义下转移所有权\n  InlineEvent(InlineEvent&& other) noexcept\n      : event_(other.event_),                        // 传递底层事件指针\n        backend_(std::move(other.backend_)),         // 移动后端实现对象\n        device_type_(other.device_type_),            // 拷贝设备类型\n        device_index_(other.device_index_),          // 拷贝设备索引\n        flag_(other.flag_),                          // 拷贝事件标志\n        was_marked_for_recording_(other.was_marked_for_recording_) {\n    // 移动后将原对象的事件指针置空，避免重复释放\n    other.event_ = nullptr;\n  }\n\n  // 移动赋值运算符，利用 swap 实现\n  InlineEvent& operator=(InlineEvent&& other) noexcept {\n    swap(other);\n    return *this;\n  }\n\n  // 交换两个 InlineEvent 对象的所有成员\n  void swap(InlineEvent& other) noexcept {\n    std::swap(event_, other.event_);\n    std::swap(backend_, other.backend_);\n    std::swap(device_type_, other.device_type_);\n    std::swap(device_index_, other.device_index_);\n    std::swap(flag_, other.flag_);\n    std::swap(was_marked_for_recording_, other.was_marked_for_recording_);\n  }\n\n  // 析构函数：若 event_ 不为空，则通过后端对象释放资源\n  ~InlineEvent() noexcept {\n    if (event_)\n      backend_.destroyEvent(event_, device_index_);\n  }\n\n  // 返回事件所属设备的类型\n  DeviceType device_type() const noexcept {\n    return device_type_;\n  }\n  \n  // 返回事件所属设备的索引\n  DeviceIndex device_index() const noexcept {\n    return device_index_;\n  }\n  \n  // 返回事件的标志\n  EventFlag flag() const noexcept {\n    return flag_;\n  }\n  \n  // 查询事件是否已被记录\n  bool was_marked_for_recording() const noexcept {\n    return was_marked_for_recording_;\n  }\n\n  // 如果尚未记录，则记录一次当前流\n  void recordOnce(const Stream& stream) {\n    if (!was_marked_for_recording_)\n      record(stream);\n  }\n\n  // 记录事件到指定流\n  void record(const Stream& stream) {\n    // 检查传入流的设备类型与事件所属设备类型是否匹配\n    TORCH_CHECK(\n        stream.device_type() == device_type_,\n        \"Event device type \",\n        DeviceTypeName(device_type_),\n        \" does not match recording stream's device type \",\n        DeviceTypeName(stream.device_type()),\n        \".\");\n    \n    // 调用后端接口进行事件记录\n    // 参数：事件指针的地址、流、当前设备索引、标志参数\n    backend_.record(&event_, stream, device_index_, flag_);\n    \n    // 标记事件已经被记录\n    was_marked_for_recording_ = true;\n    \n    // 更新设备索引为当前流的设备索引\n    device_index_ = stream.device_index();\n  }\n\n  // 阻塞等待指定流直到该事件之前的所有操作完成\n  void block(const Stream& stream) const {\n    if (!was_marked_for_recording_)\n      return;\n\n    // 检查传入流的设备类型与事件所属设备类型是否一致\n    TORCH_CHECK(\n        stream.device_type() == device_type_,\n        \"Event device type \",\n        DeviceTypeName(device_type_),\n        \" does not match blocking stream's device type \",\n        DeviceTypeName(stream.device_type()),\n        \".\");\n\n    // 调用后端接口执行阻塞等待操作\n    backend_.block(event_, stream);\n  }\n\n  // 查询事件状态：如果事件未被记录，则返回 true，否则调用后端接口查询\n  bool query() const {\n    if (!was_marked_for_recording_)\n      return true;\n    return backend_.queryEvent(event_);\n  }\n\n  // 返回底层事件指针\n  void* eventId() const {\n    return event_;\n  }\n\n  // 计算两个事件之间的经过时间（通常以毫秒为单位）\n  double elapsedTime(const InlineEvent& other) const {\n    TORCH_CHECK(\n        other.was_marked_for_recording(),\n        \"other was not marked for recording.\");\n    TORCH_CHECK(\n        was_marked_for_recording(), \"self was not marked for recording.\");\n    TORCH_CHECK(\n        other.device_type() == device_type_,\n        \"Event device type \",\n        DeviceTypeName(device_type_),\n        \" does not match other's device type \",\n        DeviceTypeName(other.device_type()),\n        \".\");\n    // 调用后端接口计算事件间耗时\n    return backend_.elapsedTime(event_, other.event_, device_index_);\n  }\n\n  // 阻塞等待当前事件完成\n  void synchronize() const {\n    if (!was_marked_for_recording_)\n      return;\n    backend_.synchronizeEvent(event_);\n  }\n\n private:\n  // 底层事件指针，通常指向后端资源（例如 CUDA 事件）\n  void* event_ = nullptr;\n  // 后端实现对象，通过模板参数 T 提供\n  T backend_;\n  // 事件所属设备的类型（例如 CPU、CUDA 等）\n  DeviceType device_type_;\n  // 事件所属设备的索引，初始为 -1 表示未设置\n  DeviceIndex device_index_ = -1;\n  // 事件的标志，默认为 PYTORCH_DEFAULT\n  EventFlag flag_ = EventFlag::PYTORCH_DEFAULT;\n  // 标识是否已经记录过事件\n  bool was_marked_for_recording_ = false;\n};\n\n```\n\n## 四 设备管理工具类-InlineDeviceGuard\n\nc10/core/impl/InlineDeviceGuard.h 文件定义了 PyTorch 中的设备管理工具类 `InlineDeviceGuard` 和 InlineOptionalDeviceGuard，用于通过 `RAII` 机制安全地设置和恢复计算设备（如 CPU/GPU）。以下是核心要点：\n1. `InlineDeviceGuard`:\n    - **作用**：在构造时将当前设备切换为指定设备，在析构时恢复到原始设备。该类通过模板参数 T（通常是具体的设备守护实现，如 CUDAGuardImpl 或 VirtualGuardImpl）来实现后端操作，从而支持静态或动态调度。\n    - **关键功能**：set_device(), reset_device(), current_device(), original_device()。\n2. `InlineOptionalDeviceGuard`:\n    - **作用**：与 InlineDeviceGuard 类似，不过它封装了一个可选的设备守护对象（使用 std::optional 包装），以支持可能未初始化的情况。\n    - **成员函数**：和 InlineDeviceGuard 类似。\n\n## 参考资料\n\n- [cuda stream and event](https://huangzhiyuan.github.io/2020/03/24/cuda-stream-and-event/index.html)\n\n"
  },
  {
    "path": "2-deep_learning_basic/pytorch_code/pytorch代码库结构拆解.md",
    "content": "---\nlayout: post\ntitle: Pytorch 代码库结构拆解\ndate: 2025-03-28 19:00:00\nsummary: pytorch 代码库结构拆解，以及核心目录的功能概述。\ncategories: Framework_analysis\n---\n\n- [1. pytorch 代码库结构](#1-pytorch-代码库结构)\n- [2. c10 核心基础库](#2-c10-核心基础库)\n- [3. ATen 模块](#3-aten-模块)\n- [参考资料](#参考资料)\n\n## 1. pytorch 代码库结构\n\nPyTorch 2.x 的源码主要划分为多个顶级目录，每个目录承担不同的功能，通过 `tree -L 1 -d` 显示当前目录的 `1` 层子目录。\n\n```bash\n├── android # 在 Android 平台上编译和部署 PyTorch 有关\n├── aten    # ATen (“A Tensor”) 是 PyTorch 的张量库与算子库核心，实现了底层的张量数据结构、算子等基础。\n├── benchmarks # 存放性能基准测试（benchmark）脚本及相关工具\n├── binaries   # 存放编译后生成的可执行文件或脚本、工具，可能也用作打包产物输出目录\n├── build      # 编译输出和中间产物\n├── c10        # PyTorch 的核心基础库，包含常用数据结构（TensorImpl, Storage）和调度器（Dispatcher）、设备适配等的通用实现。\n├── caffe2     # Caffe2 的遗留子目录，包含 Caffe2 自己的 core, utils, serialize 等部分\n├── cmake      # 存放 CMake 脚本和配置模块\n├── docs       # 存放文档\n├── functorch  #  Functorch 项目集成到 PyTorch 源码，提供可对函数进行变换（vmap, grad等）的函数式功能和原型。\n├── mypy_plugins # 存放mypy（Python 静态类型检查）的插件或自定义类型规则\n├── scripts    # 辅助脚本\n├── test       # PyTorch 的 测试用例目录：单元测试、集成测试\n├── third_party #第三方依赖库 源码\n├── tools      # 构建工具与脚本库，包含 build 系列脚本、jit 工具、onnx 工具、code_coverage, linter 等\n├── torch      # PyTorch Python 包源码的核心实现\n├── torch.egg-info\n└── torchgen   # PyTorch 算子代码生成相关脚本和生成文件目录，如算子注册、shape 函数生成、static_runtime、decompositions 等。\n```\n\n虽然第一级的子目录很多，但是对于开发者来说，最核心和重要的子目录就那几个，简单总结下其作用和相互关系：\n\n1. `c10/`：PyTorch 的**核心基础库目录**。\n   - `c10` 子目录提供了**在各平台通用的基础构件**，包括 **Tensor 元数据和存储实现、调度分发机制（dispatcher）、流（Stream）、事件（Event）等**​。\n   - 它其实是 PyTorch 和 Caffe2 合并后抽象出的统一核心层，“c10” 名字取自 “Caffe2” 与 “A Ten”的谐音（`C Ten`）。\n   - `c10` 本身不包含算子的实现，它更多的是提供一些辅助张量自动微分机制的抽象模块和类。\n2. `aten/`：`ATen` (“A Tensor”) 库目录。`ATen` 是 PyTorch 的**张量运算核心库**（`C++` 实现），提供张量及其操作的定义和实现​。它不直接包含自动求导逻辑，主要关注**张量的创建、索引、数学运算、张量运算等 `kernel` 操作和实现的功能**。`aten/src/ATen` 下有核心子目录：\n    - `ATen/core`：ATen 的核心功能（部分正逐步迁移到顶层的 `c10` 目录）。\n    - `ATen/native`：分算子（operators）的 `native` 实现。如果要新增算子，一般将实现放在这里​。根据设备类型又细分子目录:\n      - `native/cpu`: 并非真正意义上的 CPU 算子实现，而是经过特定处理器指令（如 AVX）编译的实现。​。\n      - `native/cuda`: 算子的 CUDA 实现。\n      - `native/sparse`:  COO 格式稀疏张量操作在 CPU 和 CUDA 上的实现。\n      - `native/quantized`: 量化张量（即 QTensor）算子的实现。\n3. `torch/`：真正的 PyTorch 库，除 `csrc` 目录中的内容外，其余部分都是 Python 模块，遵循 PyTorch Python 前端模块结构。\n    - `csrc`: 构成 PyTorch 库的 C++ 文件。该目录树中的文件混合了 **Python 绑定代码**和大量 `C++` 底层实现。有关 Python 绑定文件的正式列表，请参阅 `setup.py`；通常它们以 python_ 为前缀。\n     \t- `jit`: TorchScript JIT 前端的编译器及前端。一个编译堆栈（TorchScript）用于从 PyTorch 代码创建可序列化和可优化的模型。\n     \t- `autograd`: **自动求导（Autograd）系统实现**。系统的核心设计原则是为每个关键数据类型提供两套实现：C++ 类型和 Python 对象类型。以变量（Variable）为例，系统包含 variable.h 中的 Variable C++ 类型和 python_variable.h 中的 THPVariable Python 类型。\n     \t- `api`: PyTorch 的 C++ 前端。\n     \t- `distributed`: PyTorch 的分布式训练支持。\n4. `tools`: 供 PyTorch 库使用的代码生成脚本。\n\n![pytorch_src](../../images/pytorch/pytorch_src.png)\n\n## 2. c10 核心基础库\n\n`c10` 作为 PyTorch 框架的**核心基础库**，其包含多个子模块：\n- `c10/core/`：核心组件，定义了 PyTorch **核心数据结构和机制**。例如包含 `TensorImpl`（张量底层实现类）​、`Storage`（张量存储）、`DispatchKey` 和 `Dispatcher`（动态算子调度）、设备类型 `Device`、类型元信息 `TypeMeta` 等基础定义。\n- `c10/util/`：工具模块，提供通用的 C++ 实用组件。如 intrusive_ptr 智能指针、`UniqueVoidPtr` 通用指针封装、`Exception` 异常处理、日志和元编程工具等，供整个框架使用。\n- `c10/macros/`：宏定义模块，包含编译配置相关的宏。例如根据操作系统和编译选项生成的 cmake_macros.h，以及 C10_API, TORCH_API, CAFFE2_API 等符号导出控制宏​。\n- `c10/cuda/`, c10/hip/, c10/metal/, c10/xpu/ 等：特定设备平台支持代码, 这些目录有助于在 c10 层面适配不同硬件平台。例如:\n\t- c10/cuda 中包含 **CUDA 后端初始化、流管理等与 CUDA 设备相关的基础功能**；\n\t- c10/hip 类似地对应 AMD 的 HIP；\n\t- c10/metal 针对苹 Metal 后端；\n\t- c10/xpu 则可能用于其他加速器（如 Intel XPUs）。\n- `c10/mobile/`：移动端支持代码，为在移动/嵌入式场景下裁剪和优化 PyTorch 而设。\n- `c10/test/`：c10 本身的一些单元测试代码。\n\n## 3. ATen 模块\n\naten/（A Tensor Library）是 PyTorch 的核心组件，负责实现 Tensor 计算、自动微分（Autograd）、跨后端算子分发、算子在各个设备（cuda、cpu）上具体实现（aten/src/ATen/native/）。\n\naten/src/ATen/目录提供了 aten 模块的具体代码实现，核心子目录的作用如下所示：\n- core/ ：核心函数库，逐步往 c10迁移中。定义 Tensor 的核心数据结构、类型系统和 API。\n- native/：原生算子库。各后端（CPU、CUDA、XLA 等）的算子实际实现（如卷积、矩阵乘法）。\n- autograd/：自动微分引擎（如 Variable、Function 的实现）。\n- vulkan/、metal/：移动端和 GPU 后端支持。\n- quantized/：量化算子实现。\n\naten/src/ATen/core 子模块\n- 作用：定义 Tensor 的核心数据结构、类型系统和基础接口。\n- 关键文件：\n  - Tensor.h：Tensor 类的定义（核心分析对象）。\n  - TensorBase.h：Tensor 的基类，提供轻量级接口。\n  - DispatchKeySet.h：操作符分派机制（如 CPU/CUDA/Autograd 的动态分派）。\n  - ScalarType.h：数据类型（dtype）的枚举定义。\n\naten/src/ATen/native 子模块\n- 作用：各后端的原生算子实现。\n- 关键子目录：\n  - cpu/：CPU 算子实现（如 Conv2d.cpp）。\n  - cuda/：CUDA 算子实现（如 CUDABlas.cpp）。\n  - Composite/：组合算子（由基础算子拼接而成）。\n  - meta/：元算子（用于形状推导）。\n\n![aten_code_summary](../../images/pytorch/aten_code_summary.png)\n\n## 参考资料\n\n- [万字综述，核心开发者全面解读PyTorch内部张量机制](https://mp.weixin.qq.com/s/8J-vsOukt7xwWQFtwnSnWw)"
  },
  {
    "path": "2-deep_learning_basic/pytorch_code/pytorch张量实现分析.md",
    "content": "---\nlayout: post\ntitle: Pytorch 张量实现分析\ndate: 2025-03-29 19:00:00\nsummary: pytorch 张量的属性、底层实现分析以及应用，内容持续更新中。\ncategories: Framework_analysis\n---\n\n## 1. torch.Tensor 属性总结\n\n`PyTorch` 框架本质上是一个支持自动微分的张量库，张量是 PyTorch 中的核心数据结构（`torch.Tensor`），其本质上是一种多维数组数据结构，拥有一组内置的属性来描述张量的**元信息**，如数据类型（`dtype`）、存储设备（`device`）、维度信息（`shape/size`）、内存布局（`layout`）等。\n\n<div align=\"center\">\n<img src=\"../images/pytorch/tensor_define.png\" width=\"60%\" alt=\"tensor_define\">\n</div>\n\n张量核心属性总结：\n\n| 属性名          | 含义                                                | 获取方式                         | 示例                                  |\n| --------------- | --------------------------------------------------- | -------------------------------- | ------------------------------------- |\n| `shape`         | 张量各维度大小                                      | `tensor.shape` / `tensor.size()` | `torch.Size([2, 3])`                  |\n| `dtype`         | 张量中元素的数据类型                                | `tensor.dtype`                   | `torch.float32`, `torch.int64`        |\n| `device`        | 张量存储所在的设备                                  | `tensor.device`                  | `cpu`, `cuda:0`                       |\n| `requires_grad` | 是否被 autograd 记录梯度,用于构建计算图并反向传播。 | `tensor.requires_grad`           | `True` / `False`                      |\n| `layout`        | 描述张量在内存中的存储格式                          | `tensor.layout`                  | `strided`(连续内存)、sparse(稀疏存储) |\n| `is_leaf`       | 是否为计算图叶子节点                                | `tensor.is_leaf`                 | `True` / `False`                      |\n| `storage`       | 张量的底层数据存储                                  | `tensor.storage()`               | `<torch.FloatStorage object>`         |\n| `stride`        | 表示张量在各维度上移动一个元素所需跨越的内存步长    | `tensor.stride()`                | `(3, 1)`                              |\n| `is_contiguous` | 是否在内存中连续                                    | `tensor.is_contiguous()`         | `True` / `False`                      |\n\n查看张量属性实例代码：\n\n```python\nimport torch\n\n# 创建一个张量，并指定 dtype、device、requires_grad\nx = torch.tensor([[1, 2, 3], [4, 5, 6]],\n                 dtype=torch.float32,\n                 device='cpu',\n                 requires_grad=True)\n\n# 查看各属性\nprint(\"Shape:\", x.shape)              # torch.Size([2, 3])\nprint(\"Dtype:\", x.dtype)              # torch.float32 \nprint(\"Device:\", x.device)            # cpu\nprint(\"Requires Grad:\", x.requires_grad)  # True \nprint(\"Layout:\", x.layout)            # torch.strided \nprint(\"Is Leaf:\", x.is_leaf)          # True \nprint(\"Storage:\", x.storage())        # [torch.storage.TypedStorage(dtype=torch.float32, device=cpu) of size 6]\nprint(\"Stride:\", x.stride())          # (3, 1)\n```\n\n## 2. torch.Tensor 实现分析\n\nPyTorch 中的张量在 C++ 层分布通过 `ATen`（A Tensor Library）和 `C10`（核心库）来实现，具体来说:\n\n- `ATen`（A Tensor Library）：是 PyTorch 中**封装张量运算**的一层抽象，核心类型为 `at::Tensor`，并通过 aten/src/ATen/native/ 中的各种 C++ 函数（如 TensorShape.cpp、TensorAdvancedIndexing.cpp 等）实现具体运算。ATen 负责将高阶的张量运算映射到底层内核，常称之为 “张量库”。\n- `C10`：提供了张量的核心数据结构和调度机制，包括 `c10::TensorImpl`、`c10::Storage`、`c10::DispatchKey` 等。C10 还管理张量的引用计数、并为不同后端（CPU、CUDA、Sparse 等）提供了可扩展的接口。\n\n每个张量对象都包含以下关键信息：\n\n1. 数据存储 (`Storage`)：底层连续内存块，用于实际保存张量元素。\n2. 元信息 (`TensorImpl`)：包含设备信息、数据类型、维度、步幅（stride）、布局（layout）等，以及指向 Storage 的指针。\n3. 自动求导信息（`AutogradMeta`）：若张量需要梯度，则在 TensorImpl 中挂载相应的梯度追踪结构。\n4. `Dispatch Key`：用于根据数据类型、设备、布局等信息分发到相应的后端内核。\n\n\n其中张量元信息类 TensorImpl 对象至少包含以下字段：\n\n- `Device device_`：记录张量所在设备（CPU、CUDA:0、CUDA:1 等）。\n- ScalarType dtype_：张量元素的数据类型（如 Float, Double, Int, Bool 等）。\n- TensorStorage storage_：指向底层 Storage 对象，用于实际存储张量数据。\n- IntArrayRef sizes_：张量各维度的长度。\n- IntArrayRef strides_：张量各维度的步幅，用于根据索引计算数据偏移。\n- Layout layout_：表示张量存储布局，如稠密（strided）、稀疏（sparse_coo）、MKLDNN 等\n- DispatchKeySet key_set_：用于调度运算到正确的内核，比如 CPUTensorId、CUDATensorId、Autograd 等。\n\n## 3. torch.Tensor 属性实现分析\n\n对于 `dtype` 属性，PyTorch 在 C++ 端使用枚举类型 `at::ScalarType` 来表示各种数据类型（如 `Float, Double, Int` 等），然后通过 Python API 暴露为 torch.float32、torch.int64 等 Python 对象；访问 tensor.dtype 时，底层会查询对应的 at::TensorImpl 中保存的 scalar_type 字段并将其封装为 Python 对象返回。pytorch/c10/core/ScalarType.h 头文件中的核心宏 `AT_FORALL_SCALAR_TYPES_WITH_COMPLEX_AND_QINTS` 定义了一个包含45种不同标量类型的完整列表，从基础的 uint8_t（Byte）到最新的 8位浮点格式如 Float8_e4m3fn 和量化类型。\n\n对于 `device`，底层同样在 `at::TensorImpl` 中保存一个 `Device` 对象，Python API 访问时将其包装为 torch.device 对象；当执行张量迁移（如 .to(device)、.cuda()、.cpu()）时，PyTorch 会在底层拷贝或移动张量数据到指定设备，并更新 TensorImpl 中的 device 字段。\n\n对于 `shape / size`，底层使用 `TensorImpl` 中的维度信息（`sizes_` 数组）来存储；Python 访问时会将该数组封装为 torch.Size 对象（本质上是 Python 的 tuple 子类）返回。\n\n对于 requires_grad / grad，PyTorch 在创建张量时会根据 requires_grad 参数决定是否为该张量分配梯度存储；在执行反向传播时（tensor.backward()），底层的 Autograd 引擎会自动追踪与该张量相关的运算，并将梯度信息累积到 tensor.grad 属性中。\n"
  },
  {
    "path": "2-deep_learning_basic/pytorch_code/pytorch架构概览.md",
    "content": "- [一 pytorch 框架概述](#一-pytorch-框架概述)\n  - [1.1 pytorch 概述](#11-pytorch-概述)\n  - [1.2 pytorch 前后端](#12-pytorch-前后端)\n  - [1.3 在 macOS 上的编译安装](#13-在-macos-上的编译安装)\n- [二 pytorch 源码目录](#二-pytorch-源码目录)\n  - [2.1 pytorch 核心目录概述](#21-pytorch-核心目录概述)\n  - [2.2 c10 核心基础库](#22-c10-核心基础库)\n  - [2.3 \\_C 模块概述](#23-_c-模块概述)\n- [三 从 torch.tensor 理解前后端交互](#三-从-torchtensor-理解前后端交互)\n  - [\"import torch\" 时重要的初始化](#import-torch-时重要的初始化)\n  - [`torch._C` 分析](#torch_c-分析)\n  - [torch.Tensor 类的初始化和实现](#torchtensor-类的初始化和实现)\n  - [Python 3.x 扩展模块初始化规范](#python-3x-扩展模块初始化规范)\n- [参考资料](#参考资料)\n\n## 一 pytorch 框架概述\n\n### 1.1 pytorch 概述\n\n`PyTorch` 框架本质上是一个支持自动微分的张量库，张量是 PyTorch 中的核心数据结构, 它可以从两个角度理解：\n1. 直观上理解，其是一种包含某种标量类型（比如浮点数和整型数等）的 n 维数据结构。\n2. 代码实现上理解，张量可以看作是包含一些数据成员的结构体/类对象，比如，张量的尺寸、张量的元素类型（dtype）、张量所在的设备类型（CPU 内存？CUDA 内存？）和步长 `stride` 等数据成员。\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/tensor_define.png\" width=\"60%\" alt=\"tensor_define\">\n</div>\n\n神经网络模型训练的两个主要特征：\n- 张量计算 kernel 实现\n- 自动微分系统实现\n\nPytorch 称为唯一主流的训练框架最主要的原因是它**支持动态图**，以及简单易用易开发。动态图的优点是支持实时打印张量计算结果。在 PyTorch中，我们之所以可以在计算的任意步骤直接输出结果。原因是，**PyTorch 每一条语句是同步执行的**，即每一条语句都是一个（或多个）算子，被调用时实时执行。这种实时执行算子的方式我们称为动态图或算子模式。\n\n下图左侧部分总结了 pytorch 代码的执行流程，主要分为前端 python 部分和后端 c++ 部分。\n\n![pytorch_exec_process](../../images/pytorch/pytorch_exec_process.png)\n\n### 1.2 pytorch 前后端\n\n在 pytorch 中前端指的是 pytorch 的 python  接口，用于构建数据集处理 pipeline、定义模型结构和训练评估模型的工具接口。\n\n后端指的是 PyTorch 的底层 C++ 引擎，它负责执行前端指定的计算。后端引擎使用张量表示计算图的节点和边，并使用高效的线性代数运算和卷积运算来执行计算。后端引擎支持多设备（如 cpu、cuda、rocm等）执行计算，将 python 计算代码转换为底层设备平台能够执行的代码。\n\n### 1.3 在 macOS 上的编译安装\n\n1，前置安装条件：\n\n`Cmake` 使用 `brew` 方式安装，不推荐用 `conda` 安装，版本优先推荐 `3.31`:\n```bash\n(torch) honggao@U-9TK992W3-1917 pytorch % cmake --version\ncmake version 3.31.6 \n```\n\n2，在环境中有 conda 的基础上，按照以下步骤编译按照 cpu 版本的 pytorch。\n\n```bash\n# 1, 创建名为 torch_dev 的虚拟环境，指定 python 版本为 3.12\nconda create --name torch_dev python=3.12\nconda activate torch_dev # 可不执行\n\n# 2，安装依赖包\nconda install cmake ninja\npip install -r requirements.txt\n# 如果需要 torch.distributed 模块的功能\nconda install pkg-config libuv\n\n# 如果本地电脑 macos 系统中没有 cmake 或者版本低于3.15\nbrew install cmake\n\n# 3,编译安装 torch 包（这个过程耗时非常严重）\nMAX_JOBS=8 python setup.py develop # 使用 8 个并行任务同时编译\nMAX_JOBS=8 python setpy.py install 编译源码，打好包，并安装（拷贝）到平台 python 环境中\n```\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/pytorch_build.jpg\" width=\"50%\" alt=\"result\">\n</div>\n\n和 `install` 方式不同之处在于，其不拷贝文件到平台环境，而是在 python 启动时直接将源码目录加到 python PATH 变量中。在 python 解释中通过 import sys 并打印 sys.path 可以看到**pytorch 多了源码目录**。\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/sys_path.jpg\" width=\"100%\" alt=\"sys_path\">\n</div>\n\n`develop` 编译安装方式可以让开发者在开发顶层 `python` 层逻辑代码时（比如要加入一个新的 nn module layer），**在源码处做更改后即可立刻看到效果**。\n\n举个例子，我在 pytorch python 端的 Tensor 类代码的 `__len__` 魔法方法中加入一段 print(\"Get torch len is need to call self.shape[0]\") 代码后，在 python 中调用 `len(tensor)` 会立即生效，如下图所示:\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/update_tensor_len.jpg\" width=\"60%\" alt=\"update_tensor_len\">\n</div>\n\n## 二 pytorch 源码目录\n\nPyTorch 2.x 的源码主要划分为多个顶级目录，每个目录承担不同的功能，通过 `tree -L 1 -d` 显示当前目录的 `1` 层子目录。\n\n```bash\n├── android # 在 Android 平台上编译和部署 PyTorch 有关\n├── aten    # ATen (“A Tensor”) 是 PyTorch 的张量库与算子库核心，实现了底层的张量数据结构、算子等基础。\n├── benchmarks # 存放性能基准测试（benchmark）脚本及相关工具\n├── binaries   # 存放编译后生成的可执行文件或脚本、工具，可能也用作打包产物输出目录\n├── build      # 编译输出和中间产物\n├── c10        # PyTorch 的核心基础库，包含常用数据结构（TensorImpl, Storage）和调度器（Dispatcher）、设备适配等的通用实现。\n├── caffe2     # Caffe2 的遗留子目录，包含 Caffe2 自己的 core, utils, serialize 等部分\n├── cmake      # 存放 CMake 脚本和配置模块\n├── docs       # 存放文档\n├── functorch  #  Functorch 项目集成到 PyTorch 源码，提供可对函数进行变换（vmap, grad等）的函数式功能和原型。\n├── mypy_plugins # 存放mypy（Python 静态类型检查）的插件或自定义类型规则\n├── scripts    # 辅助脚本\n├── test       # PyTorch 的 测试用例目录：单元测试、集成测试\n├── third_party #第三方依赖库 源码\n├── tools      # 构建工具与脚本库，包含 build 系列脚本、jit 工具、onnx 工具、code_coverage, linter 等\n├── torch      # PyTorch Python 包源码的核心实现\n├── torch.egg-info\n└── torchgen   # PyTorch 算子代码生成相关脚本和生成文件目录，如算子注册、shape 函数生成、static_runtime、decompositions 等。\n```\n\n### 2.1 pytorch 核心目录概述\n\n虽然第一级的子目录很多，但是对于开发者来说，最核心和重要的子目录就那几个，简单总结下其作用和相互关系：\n\n1. `c10/`：c10 指的是 caffe tensor library，相当于 caffe 的 aten, PyTorch 的**核心基础库目录**。\n   - c10 子目录提供了**在各平台通用的基础构件**，包括**Tensor 元数据和存储实现、调度分发机制（dispatcher）、流（Stream）、事件（Event）等**​。\n   - 它其实是 PyTorch 和 Caffe2 合并后抽象出的统一核心层，“c10” 名字取自 “Caffe2” 与 “A Ten”的谐音（`C Ten`）。\n   - c10 本身不包含算子的实现，它更多的是提供一些辅助张量自动微分机制的抽象模块和类。\n2. `aten/`：ATen (“A Tensor”) 库目录。ATen 是 PyTorch 的**张量运算核心库**（C++ 实现），提供张量及其操作的定义和实现​。它不直接包含自动求导逻辑，主要关注**张量的创建、索引、数学运算、张量运算等 kernel 操作和实现的功能**。aten/src/ATen 下有核心子目录：\n    - `ATen/core`：ATen 的核心功能（部分正逐步迁移到顶层的 c10 目录）。\n    - `ATen/native`：分算子（operators）的 `native` 实现。如果要新增算子，一般将实现放在这里​。根据设备类型又细分子目录:\n      - `native/cpu`: 并非真正意义上的 CPU 算子实现，而是经过特定处理器指令（如 AVX）编译的实现。​。\n      - `native/cuda`: 算子的 CUDA 实现。\n      - `native/sparse`:  COO 格式稀疏张量操作在 CPU 和 CUDA 上的实现。\n      - `native/quantized`: 量化张量（即 QTensor）算子的实现。\n3. `torch/`：真正的 PyTorch 库，除 csrc 中的内容外，其余部分都是 Python 模块，遵循 PyTorch Python 前端模块结构。\n    - `csrc`: 构成 PyTorch 库的 C++ 文件。该目录树中的文件混合了 Python 绑定代码和大量 C++ 底层实现。有关 Python 绑定文件的正式列表，请参阅 `setup.py`；通常它们以 python_ 为前缀。\n\t- `jit`: TorchScript JIT 前端的编译器及前端。一个编译堆栈（TorchScript）用于从 PyTorch 代码创建可序列化和可优化的模型。\n\t- `autograd`: **反向自动微分的实现**。详见 README。\n\t- `api`: PyTorch 的 C++ 前端。\n\t- `distributed`: PyTorch 的分布式训练支持。\n4. `tools`: 供 PyTorch 库使用的代码生成脚本。\n\n### 2.2 c10 核心基础库\n\nc10 作为 PyTorch 框架的**核心基础库**，其包含多个子模块：\n- `c10/core/`：核心组件，定义了 PyTorch **核心数据结构和机制**。例如包含 `TensorImpl`（张量底层实现类）​、`Storage`（张量存储）、`DispatchKey` 和 `Dispatcher`（动态算子调度）、设备类型 `Device`、类型元信息 `TypeMeta` 等基础定义。\n- `c10/util/`：工具模块，提供通用的 C++ 实用组件。如 intrusive_ptr 智能指针、`UniqueVoidPtr` 通用指针封装、`Exception` 异常处理、日志和元编程工具等，供整个框架使用。\n- `c10/macros/`：宏定义模块，包含编译配置相关的宏。例如根据操作系统和编译选项生成的 cmake_macros.h，以及 C10_API, TORCH_API, CAFFE2_API 等符号导出控制宏​。\n- `c10/cuda/`, c10/hip/, c10/metal/, c10/xpu/ 等：特定设备平台支持代码, 这些目录有助于在 c10 层面适配不同硬件平台。例如:\n\t- c10/cuda 中包含 **CUDA 后端初始化、流管理等与 CUDA 设备相关的基础功能**；\n\t- c10/hip 类似地对应 AMD 的 HIP；\n\t- c10/metal 针对苹 Metal 后端；\n\t- c10/xpu 则可能用于其他加速器（如 Intel XPUs）。\n- `c10/mobile/`：移动端支持代码，为在移动/嵌入式场景下裁剪和优化 PyTorch 而设。\n- `c10/test/`：c10 本身的一些单元测试代码。\n\n### 2.3 _C 模块概述\n\n在 PyTorch 中，`_C` 模块（通常以 `torch._C` 命名）是 PyTorch 的核心 C/C++ 层的接口。它是一个经过编译的动态库（例如在 Linux 下为 `.so` 文件，在 macOS 下为 `.dylib`，在 Windows 下为 `.dll`），用于向 Python 层暴露底层高性能实现的功能。\n\n实现原理：\n\n1. **C++/CUDA 内核实现**：PyTorch 的大部分核心运算、数据结构和算法均在 C++ 层实现，并且支持 CPU/GPU/XPU 多设备后端。\n2. **绑定机制**：使用 `pybind11` 将 `C++` 类和函数暴露为 `Python` 模块中的对象和方法。\n3. **编译与打包**：在构建过程中，通过 `setuptools` 或 `CMake` 调用编译器将 C++ 源文件编译成动态库，并在打包时将其放入 Python 包（例如 `torch/_C.so`），从而实现跨平台分发。\n\n## 三 从 torch.tensor 理解前后端交互\n\n### \"import torch\" 时重要的初始化\n\n当在 python 代码中执行 `import torch` 时，import 会去寻找 */`site-packages/torch/__init__.py` 文件，其是 PyTorch 的顶层入口文件。而 `__init__.py` 文件的作用是完成 PyTorch 的**模块初始化与全局配置和子模块加载过程**等。具体来说，负责完成以下主要任务：\n1. 模块初始化与全局配置：读取和设置版本信息、配置信息，以及初始化日志、环境变量等全局状态。\n2. 动态库加载: 加载 `_C` 模块时，会调用其中的初始化函数（例如 `PyInit__C()`），完成低层核心组件的初始化。\n3. API 封装和命名空间构建。\n4. 子模块导入：将 torch.nn、torch.optim、torch.cuda 等子模块导入到顶层命名空间。\n5. 错误处理和兼容性支持：确保在不同操作系统、不同 CUDA/ROCm 环境下的兼容性，并给出相应的警告或提示信息。\n\n`__init__.py` 文件中包含了多种与张量初始化相关的关键组件，以下是对主要部分的总结和详细解释：\n\n1. 核心 Tensor 类的导入与定义\n\n```python\nfrom torch._tensor import Tensor  # 导入核心 Tensor 类\n```\n\n这一行导入了 PyTorch 的核心 Tensor 类，它继承自 C++ 实现的 `torch._C.TensorBase`，是所有张量操作的基础。张量创建时都会实例化这个类。\n\n2. **动态链接库加载**\n\n```python\ndef _load_global_deps() -> None:\n    #############省略代码############\nif USE_GLOBAL_DEPS:\n        _load_global_deps()\n    from torch._C import *  # noqa: F403\n```\n\n3. **符号张量支持**\n\n```python\nclass SymInt: ...\nclass SymFloat: ...\nclass SymBool: ...\n\ndef sym_int(a): ... \ndef sym_float(a): ...\ndef _constrain_as_size(symbol, min=None, max=None): ...\n```\n\n4. 设置设备和数据类型配置函数定义\n\n```python    \ndef set_default_device(device):\n    # 设置默认设备，影响张量创建时的默认位置\n    _GLOBAL_DEVICE_CONTEXT.device_context = device_context\n\ndef set_default_dtype(d: \"torch.dtype\") -> None:\n    # 设置默认浮点数据类型\n```\n\n这些函数会控制新创建张量的默认设备和数据类型。例如，\n- set_default_device('cuda') 会使新张量默认创建在 GPU 上。\n- set_default_dtype(torch.float64) 会改变浮点张量的默认精度。\n- set_default_tensor_type 是一个较旧的 API，现在推荐使用 set_default_dtype 加 set_default_device 的组合来代替。\n\n5. **张量存储类定义**\n\n现在推荐使用 `TypedStorage` 和 `UntypedStorage` 作为 PyTorch storage object。\n\n```python\nfrom torch.storage import (\n    _LegacyStorage,\n    _StorageBase,\n    _warn_typed_storage_removal,\n    TypedStorage,\n    UntypedStorage,\n)\n```\n\n### `torch._C` 分析\n\n在我的 macos 电脑中对应的就是 `torch/_C.cpython-312-darwin.so` 文件，文件名指明了编译它的 python 版本和所在平台系统，这种命名是 python c module 的一种规范。\n\n### torch.Tensor 类的初始化和实现\n\n> `Python` 中的类型也是对象，类型是 `PyTypeObject` 对象。\n\ntorch/_tensor.py 是 PyTorch 中定义和包装张量（Tensor）的 Python 端接口文件，它连接了 C++ 内核实现与 Python 用户接口。总体来说，\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/Tensor.jpg\" width=\"60%\" alt=\"Tensor\">\n</div>\n\n`_TensorBase` 类的实现是在 torch/_C/__init__.pyi 中。\n\ntorch/_C/init.pyi 作文件是 PyTorch 对外提供的 C++ 扩展模块接口的**类型提示文件**（`stub file`），用于描述 torch._C 模块中 C++ 实现的各个函数、类和常量的类型定义信息，但是没有具体实现。\n\nPyTorch 通过 pybind11 将 C++ 函数、类暴露给 Python，在这个过程中部分信息可以被用来生成 `.pyi` 文件。\n\n下图可以看出 `TensorBase` 类真正的实现是在 torch/csrc/autograd/python_variable.cpp 中。\n\n<div align=\"center\">\n<img src=\"../../images/pytorch/TensorBase.jpg\" width=\"60%\" alt=\"TensorBase\">\n</div>\n\n在 python_variable.cpp 文件中，可以看到 PyTorch 实际是用了 pybind，将 C++ 和 Python 进行交互的。\n\n> `“THPVariable”` 全称为`“Torch Python Variable”`，用于表示 `PyTorch` 源码中用于 `Python` 绑定的 C 结构体类型，其代表了 Python 层中的 `Tensor` 对象（历史上称为 `Variable`）\n\npytorch python 中的 tensor 实现是继承自 _C module 的 `_TensorBase class`，而 `_TensorBase` 是在 C++ 代码中实现并添加到 `_C` 模块中，如下：\n\n```cpp\nbool THPVariable_initModule(PyObject* module) {\n  THPVariableMetaType.tp_base = &PyType_Type;\n  if (PyType_Ready(&THPVariableMetaType) < 0)\n    return false;\n  Py_INCREF(&THPVariableMetaType);\n  PyModule_AddObject(module, \"_TensorMeta\", (PyObject*)&THPVariableMetaType);\n\n  static std::vector<PyMethodDef> methods;\n  THPUtils_addPyMethodDefs(methods, torch::autograd::variable_methods);\n  THPUtils_addPyMethodDefs(methods, extra_methods);\n  THPVariableType.tp_methods = methods.data();\n  if (PyType_Ready(&THPVariableType) < 0)\n    return false;\n  Py_INCREF(&THPVariableType);\n  PyModule_AddObject(module, \"TensorBase\", (PyObject*)&THPVariableType);\n  Py_INCREF(&THPVariableType);\n  PyModule_AddObject(module, \"_TensorBase\", (PyObject*)&THPVariableType);\n  torch::autograd::initTorchFunctions(module);\n  torch::autograd::initTensorImplConversion(module);\n  torch::utils::validate_numpy_for_dlpack_deleter_bug();\n  return true;\n}\n```\n\nTHPVariable_initModule 里相关操作, 初始化 THPVariableType 类类型，增加 THPVariableType 计数等是 C API 往 python 模块里添加类类型的标准做法。\n\n下述代码是实现了往 python 的 module 中添加了名为 “TensorBase” 的类类型对象 THPVariableType，第一个参数 module 是 _C module 类型。\n\n```cpp\nPyModule_AddObject(module, \"_TensorBase\",   (PyObject *)&THPVariableType);\n```\n\n因为 Tensor 类继承自 `_TensorBase`，所以在 Tensor 被实例化之前，必须先完成 _TensorBase 的初始化。又因为 `_TensorBase` 是属于 _C 模块的一部分，所以在代码 `torch/__init__` 中，当执行 `from torch._C import * ` 时，实际上也完成了 `_TensorBase` 的初始化。\n \n当在 Python 中执行 `import torch._C`（或间接通过 import torch 导入时）时，Python 解释器会自动调用 `PyInit__C()` 来初始化该模块。\n\n`PyInit__C()` 的具体实现是在 `torch/csrc/stub.c` （关键源文件）中，其包含以下内容：\n\n- **构建过程**：在编译过程中，`stub.cpp` 会被编译成目标文件（.o 文件），与其他目标文件一起链接生成最终的动态库，例如文件名为 `_C.python-37m-x86_64-linux-gnu.so` 的共享库。\n- **多个目标文件**：通常构建这样的大型项目时，除了 `stub.c` 之外，还会有其他源文件编译生成目标文件。这些目标文件一起被链接成最终的动态库 `_C.python-37m-x86_64-linux-gnu.so`，其中 `stub.c` 中包含的 `PyInit__C()` 函数就是模块初始化的入口。\n\n`torch/csrc/stub.c` 文件内容如下：\n\n```cpp\n#include <Python.h>\n\nextern PyObject* initModule(void);\n\n#ifndef _WIN32\n#ifdef __cplusplus\nextern \"C\"\n#endif\n__attribute__((visibility(\"default\"))) PyObject* PyInit__C(void);\n#endif\n\nPyMODINIT_FUNC PyInit__C(void)\n{\n  return initModule();\n}\n```\n\n### Python 3.x 扩展模块初始化规范\n\n根据 Python 3.x 的 C API 规范，每个用 C 或 C++ 编写的扩展模块都必须提供一个初始化函数，该函数的名称必须以 `PyInit_` 开头，后面紧跟模块的名称。例如：\n- 如果模块名称为 `_C`，那么初始化函数的名称就必须是 `PyInit__C()`。\n- 这个函数在模块导入时由 Python 解释器自动调用，用来创建并返回一个模块对象。\n\n假设我们有一个 C 扩展模块初始化函数 PyInit__C，其部分代码如下:\n\n```cpp\nstatic struct PyModuleDef moduledef = {\n    PyModuleDef_HEAD_INIT,\n    \"_C\",                /* m_name */\n    \"Module for PyTorch core.\", /* m_doc */\n    -1,                  /* m_size */\n    NULL,                /* m_methods */\n    NULL,                /* m_reload */\n    NULL,                /* m_traverse */\n    NULL,                /* m_clear */\n    NULL,                /* m_free */\n};\n\nPyMODINIT_FUNC PyInit__C(void) {\n    PyObject *module = PyModule_Create(&moduledef);\n    if (!module)\n        return NULL;\n    \n    // 将 THPVariableType 绑定为 _TensorBase 属性\n    if (PyModule_AddObject(module, \"_TensorBase\", (PyObject *)&THPVariableType) < 0) {\n        Py_DECREF(module);\n        return NULL;\n    }\n    \n    // ... 其他初始化代码 ...\n    \n    return module;\n}\n```\n\n初始化函数会负责执行模块内的各项初始化操作，比如设置模块的全局变量、注册函数、创建类型对象等。只有初始化函数返回一个有效的模块对象后，Python 才能将该模块添加到全局命名空间中供后续使用。\n\n`_C` 模块表示是用 `C++` 编写的 `Python` 模块，根据 Python 3.x 的 API 规范，模块的初始化入口必须以 `“PyInit”` 作为前缀，紧跟模块名称。在这个例子中，对应的初始化函数即为 `PyInit__C()`。该函数正是定义在 `stub.cpp` 文件中，而这\n\n\n\n## 参考资料\n\n- [万字综述，核心开发者全面解读PyTorch内部张量机制](https://mp.weixin.qq.com/s/8J-vsOukt7xwWQFtwnSnWw)\n- [【Pytorch 源码 Detail 系列】Tensor“函数工厂” 上](https://zhuanlan.zhihu.com/p/346926464)\n- [Let’s talk about the PyTorch dispatcher](https://blog.ezyang.com/2020/09/lets-talk-about-the-pytorch-dispatcher/)"
  },
  {
    "path": "2-deep_learning_basic/pytorch_code/pytorch编译流程解析.md",
    "content": "- [1. 解析 setup.py 中 main 函数流程](#1-解析-setuppy-中-main-函数流程)\n- [2. configure\\_extension\\_build 函数流程](#2-configure_extension_build-函数流程)\n\n## 1. 解析 setup.py 中 main 函数流程\n\n在 `setup.py` 中，最主要的是调用 `setuptools.setup()` 函数，它是 Python 中 setuptools 模块的核心，**用于定义 Python 包的元数据、依赖关系和构建流程**。pytorch/setup.py 在 `main()` 函数中调用了 `setup()` 函数。\n\n`setup` 的函数参数有很多个，主要分为以下类型：\n1. **基础参数**：定义包元数据：包名称 `name`、版本号 `version`、简要描述 description、作者 author、项目主页 url、子包 packages、依赖的第三方库 install_requires、长描述 long_description 等。\n2. **扩展模块参数** `ext_modules`：编译 `C++/CUDA` 代码。\n3. **自定义构建流程** `cmdclass`：覆盖默认命令。\n\n这里对其进行了注释如下所示：\n\n```python\nsetup(\n    # -------------------------------------------\n    # 1. 基础信息\n    # -------------------------------------------\n    name=package_name,                 # 包名，在 PyPI 或安装时用于标识该包，比如 \"torch\"\n    version=version,                   # 包版本号，通常遵循语义化版本(如 1.0.0)\n    description=(                      # 简短描述，会显示在 PyPI 中的标题描述\n        \"Tensors and Dynamic neural networks in Python with strong GPU acceleration\"\n    ),\n    long_description=long_description, # 包的详细描述，通常从 README.md 中读取，可在 PyPI 上显示更丰富内容\n    long_description_content_type=\"text/markdown\",  # 指定 long_description 的格式，这里是 Markdown\n\n    # -------------------------------------------\n    # 2. 编译和构建相关\n    # -------------------------------------------\n    ext_modules=extensions,   # 需要编译的 C/C++ 扩展模块列表（如 pybind11、Cython 等）\n    cmdclass=cmdclass,        # 自定义构建/安装命令的类映射，覆盖默认的 build_ext 等逻辑\n\n    # -------------------------------------------\n    # 3. Python 包及入口点\n    # -------------------------------------------\n    packages=packages,        # 要被打包的 Python 包列表，通常由 find_packages() 获取\n    entry_points=entry_points,# 安装包时可注册命令行可执行入口，如 console_scripts\n\n    # -------------------------------------------\n    # 4. 依赖和可选依赖\n    # -------------------------------------------\n    install_requires=install_requires, # 安装该包时必须安装的依赖库列表\n    extras_require=extras_require,      # 可选依赖项：用户可通过 \"pip install package[extra]\" 安装\n\n    # -------------------------------------------\n    # 5. 包含的文件/资源\n    # -------------------------------------------\n    package_data=package_data,          # 指定需要包含在包内的额外文件（如头文件、.so/.dll、.cmake等）\n                                        # 与实际的 Python 源码一起打包发布\n\n    # -------------------------------------------\n    # 6. 版权、主页及其他元数据\n    # -------------------------------------------\n    url=\"https://pytorch.org/\",         # 包对应的项目网址，会在 PyPI 上显示\n    download_url=\"https://github.com/pytorch/pytorch/tags\", # 包源码下载地址\n    author=\"PyTorch Team\",              # 作者姓名\n    author_email=\"packages@pytorch.org\",# 作者联系邮箱\n\n    # -------------------------------------------\n    # 7. Python 版本和分类器\n    # -------------------------------------------\n    python_requires=f\">={python_min_version_str}\", # 指定兼容的最低 Python 版本，比如 \">=3.6\"\n    classifiers=[\n        # PyPI 上展示的分类器，可帮助其他人快速了解该库的适用范围、状态和许可证等\n        \"Development Status :: 5 - Production/Stable\",\n        \"Intended Audience :: Developers\",\n        \"Intended Audience :: Education\",\n        \"Intended Audience :: Science/Research\",\n        \"License :: OSI Approved :: BSD License\",\n        \"Topic :: Scientific/Engineering\",\n        \"Topic :: Scientific/Engineering :: Mathematics\",\n        \"Topic :: Scientific/Engineering :: Artificial Intelligence\",\n        \"Topic :: Software Development\",\n        \"Topic :: Software Development :: Libraries\",\n        \"Topic :: Software Development :: Libraries :: Python Modules\",\n        \"Programming Language :: C++\",\n        \"Programming Language :: Python :: 3\",\n    ]\n    + [\n        # 额外添加 \"Programming Language :: Python :: 3.X\" 的分类器\n        f\"Programming Language :: Python :: 3.{i}\"\n        for i in range(python_min_version[1], version_range_max)\n    ],\n\n    # -------------------------------------------\n    # 8. 许可证与关键词\n    # -------------------------------------------\n    license=\"BSD-3-Clause\",      # 指定许可证，这里为 BSD-3-Clause\n    keywords=\"pytorch, machine learning\",  # 关键词，用于 PyPI 搜索\n)\n```\n\n`main()` 函数的流程图如下所示：\n\n```mermaid\nflowchart TD\n    A[1.检查编译模式冲突] -->|BUILD_PYTHON_ONLY 和 BUILD_LIBTORCH_WHL 只能二选一| B(2.定义基础python依赖包列表)\n    B --> C(3.ARM + CUDA 平台的链接脚本优化设置)\n    C --> |第二个参数通常是 build, install, develop 等| D(4.解析命令行参数并检查有效性)\n    D --> |mirror_files_into_torchgen 函数拷贝文件| E(5.预处理：拷贝必要文件到 torchgen，及构建第三方依赖)\n    E --> |调用 configure_extension_build 函数| F(6.配置 C/C++ 扩展编译信息，获取扩展模块及额外依赖)\n    F --> G(7.读取 README.md 作为 long_description)\n    G --> H(8.构建要打包进 Python 包内的基础数据列表 package_data)\n    H --> S{根据编译选项 BUILD_PYTHON_ONLY 等选择打包的动态库和头文件}\n    S --> J1(把 libtorch_python.* 动态库也一起打包l)\n    S --> J2(打包其他 lib/.so/.dll/.lib 等)\n    S --> J3(包含 tensorpipe 的头文件)\n    S --> J4(包含 kineto 的头文件)\n    S --> J5(如果不是构建 libtorch wheel，就把 torchgen 相关也打包)\n    J1 --> I(9.调用 setuptools.setup 函数执行 Python 包的打包/安装)\n    J2 --> I(9.调用 setuptools.setup 执行 Python 包的打包/安装)\n    J3 --> I(9.调用 setuptools.setup 执行 Python 包的打包/安装)\n    J4 --> I(9.调用 setuptools.setup 执行 Python 包的打包/安装)\n    J5 --> I(9.调用 setuptools.setup 执行 Python 包的打包/安装)\n    I --> J(10.如果需要，打印构建警告或提示信息)\n```\n\npackage_data 是存放要包含在 'torch' 包中的文件(二进制文件、头文件、cmake脚本等)，分两种情况：\n1. 如果不是构建 libtorch wheel，就把 libtorch_python.* 动态库也一起打包；\n2. 如果不是纯 Python 构建，那么还要包含其他 .so/.dll/.lib 等；\n3. 如果开启 USE_TENSORPIPE，就把 tensorpipe 的头文件也包含进来；\n4. 如果开启 USE_KINETO，就把 kineto 的头文件也包含进来。\n5. 若不是构建 libtorch wheel，就把 torchgen 相关也打包；否则不包含扩展\n\n为了更方便理解 main 函数流程，我对 torch/setup.py 的 main 做了简化并添加注释，代码如下所示:\n\n```python\ndef main():\n    # --------------------------------------------------------------\n    # 1. 检查编译模式冲突\n    # --------------------------------------------------------------\n    # 如果指定了同时只构建 Python 部分 (BUILD_PYTHON_ONLY)\n    # 和构建 libtorch wheel 包 (BUILD_LIBTORCH_WHL)，两者互斥，抛出异常\n    if BUILD_LIBTORCH_WHL and BUILD_PYTHON_ONLY:\n        raise RuntimeError(\n            \"Conflict: 'BUILD_LIBTORCH_WHL' and 'BUILD_PYTHON_ONLY' can't both be 1. Set one to 0 and rerun.\"\n        )\n\n    # --------------------------------------------------------------\n    # 2. 定义基础依赖列表\n    # --------------------------------------------------------------\n    # 安装 PyTorch 时需要安装的 Python 包依赖\n    install_requires = [\n        \"filelock\",\n        \"typing-extensions>=4.10.0\",\n        'setuptools ; python_version >= \"3.12\"',\n        \"sympy>=1.13.3\",\n        \"networkx\",\n        \"jinja2\",\n        \"fsspec\",\n    ]\n\n    # 如果只构建 Python，那么需要对应的 libtorch 包\n    if BUILD_PYTHON_ONLY:\n        install_requires.append(f\"{LIBTORCH_PKG_NAME}=={get_torch_version()}\")\n\n    # --------------------------------------------------------------\n    # 3. ARM + CUDA 平台的链接脚本优化设置\n    # --------------------------------------------------------------\n    use_prioritized_text = str(os.getenv(\"USE_PRIORITIZED_TEXT_FOR_LD\", \"\"))\n    # 如果在 Linux + AArch64 上未设置 USE_PRIORITIZED_TEXT_FOR_LD，就提示一下优化策略\n    if (\n        use_prioritized_text == \"\"\n        and platform.system() == \"Linux\"\n        and platform.processor() == \"aarch64\"\n    ):\n        print_box(\n            \"\"\"\n            WARNING: we strongly recommend enabling linker script optimization for ARM + CUDA.\n            To do so please export USE_PRIORITIZED_TEXT_FOR_LD=1\n            \"\"\"\n        )\n    # 如果启用 USE_PRIORITIZED_TEXT_FOR_LD，则调用脚本生成链接脚本并注入编译/链接选项\n    if use_prioritized_text == \"1\" or use_prioritized_text == \"True\":\n        gen_linker_script(\n            filein=\"cmake/prioritized_text.txt\", fout=\"cmake/linker_script.ld\"\n        )\n        linker_script_path = os.path.abspath(\"cmake/linker_script.ld\")\n        os.environ[\"LDFLAGS\"] = os.getenv(\"LDFLAGS\", \"\") + f\" -T{linker_script_path}\"\n        os.environ[\"CFLAGS\"] = (\n            os.getenv(\"CFLAGS\", \"\") + \" -ffunction-sections -fdata-sections\"\n        )\n        os.environ[\"CXXFLAGS\"] = (\n            os.getenv(\"CXXFLAGS\", \"\") + \" -ffunction-sections -fdata-sections\"\n        )\n\n    # --------------------------------------------------------------\n    # 4. 解析命令行参数并检查有效性\n    # --------------------------------------------------------------\n    # Distribution 对象可解析命令行参数 (如 build, install, sdist, bdist_wheel 等)\n    dist = Distribution()\n    dist.script_name = os.path.basename(sys.argv[0])\n    dist.script_args = sys.argv[1:]\n    try:\n        dist.parse_command_line()\n    except setuptools.distutils.errors.DistutilsArgError as e:\n        print(e)\n        sys.exit(1)\n\n    # --------------------------------------------------------------\n    # 5. 预处理：拷贝必要文件到 torchgen，及构建第三方依赖\n    # --------------------------------------------------------------\n    mirror_files_into_torchgen()\n    if RUN_BUILD_DEPS:\n        build_deps()\n\n    # --------------------------------------------------------------\n    # 6. 配置 C/C++ 扩展编译信息，获取扩展模块及额外依赖\n    # --------------------------------------------------------------\n    (\n        extensions,\n        cmdclass,\n        packages,\n        entry_points,\n        extra_install_requires,\n    ) = configure_extension_build()\n    # 将额外依赖追加到基础依赖中\n    install_requires += extra_install_requires\n\n    # 扩展依赖项(可选安装)，比如 pip install torch[optree]\n    extras_require = {\n        \"optree\": [\"optree>=0.13.0\"],\n        \"opt-einsum\": [\"opt-einsum>=3.3\"],\n    }\n\n    ########## 读取 README.md 作为 long_description的代码省略 ##########\n    # --------------------------------------------------------------\n    # 7. 构建要打包进 Python 包内的数据列表(package_data)\n    # --------------------------------------------------------------\n    # version_range_max 用于动态生成 Python 版本兼容的分类器\n    version_range_max = max(sys.version_info[1], 13) + 1\n\n    # torch_package_data 存放要包含在 'torch' 包中的文件(二进制文件、头文件、cmake脚本等)\n    torch_package_data = [\n        \"py.typed\",\n        \"bin/*\",\n        \"test/*\",\n        \"*.pyi\",\n        \"**/*.pyi\",\n        \"lib/*.pdb\",\n        \"lib/**/*.pdb\",\n        \"lib/*shm*\",\n        \"lib/torch_shm_manager\",\n        \"lib/*.h\",\n        \"lib/**/*.h\",\n        \"include/*.h\",\n        \"include/**/*.h\",\n        \"include/*.hpp\",\n        \"include/**/*.hpp\",\n        \"include/*.cuh\",\n        \"include/**/*.cuh\",\n        \"_inductor/codegen/*.h\",\n        \"_inductor/codegen/aoti_runtime/*.cpp\",\n        \"_inductor/script.ld\",\n        \"_export/serde/*.yaml\",\n        \"_export/serde/*.thrift\",\n        \"share/cmake/ATen/*.cmake\",\n        \"share/cmake/Caffe2/*.cmake\",\n        \"share/cmake/Caffe2/public/*.cmake\",\n        \"share/cmake/Caffe2/Modules_CUDA_fix/*.cmake\",\n        \"share/cmake/Caffe2/Modules_CUDA_fix/upstream/*.cmake\",\n        \"share/cmake/Caffe2/Modules_CUDA_fix/upstream/FindCUDA/*.cmake\",\n        \"share/cmake/Gloo/*.cmake\",\n        \"share/cmake/Tensorpipe/*.cmake\",\n        \"share/cmake/Torch/*.cmake\",\n        \"utils/benchmark/utils/*.cpp\",\n        \"utils/benchmark/utils/valgrind_wrapper/*.cpp\",\n        \"utils/benchmark/utils/valgrind_wrapper/*.h\",\n        \"utils/model_dump/skeleton.html\",\n        \"utils/model_dump/code.js\",\n        \"utils/model_dump/*.mjs\",\n    ]\n\n    ##########################省略部分代码############################\n\n    # 若不是构建 libtorch wheel，就把 torchgen 相关也打包；否则不包含扩展\n    if not BUILD_LIBTORCH_WHL: # BUILD_LIBTORCH_WHL 默认为 0\n        package_data[\"torchgen\"] = torchgen_package_data\n    else:\n        # 在 BUILD_LIBTORCH_WHL 模式下，不需要构建Python C/C++扩展\n        extensions = []\n\n    # --------------------------------------------------------------\n    # 8. 调用 setuptools.setup() 执行 Python 包的打包/安装\n    # --------------------------------------------------------------\n    setup(\n        name=package_name,\n        version=version,\n        description=(\n            \"Tensors and Dynamic neural networks in Python with strong GPU acceleration\"\n        ),\n        ext_modules=extensions,           # C/C++ 扩展模块\n        cmdclass=cmdclass,               # 自定义构建命令\n        packages=packages,               # 包含的Python包\n        entry_points=entry_points,       # 可执行入口\n        install_requires=install_requires,\n        extras_require=extras_require,\n        package_data=package_data,\n        url=\"https://pytorch.org/\",\n        download_url=\"https://github.com/pytorch/pytorch/tags\",\n        author=\"PyTorch Team\",\n        author_email=\"packages@pytorch.org\",\n        python_requires=f\">={python_min_version_str}\",\n        # classifiers 列表和 long_description 代码省略\n        license=\"BSD-3-Clause\",\n        keywords=\"pytorch, machine learning\",\n    )\n\nif __name__ == \"__main__\":\n    main()\n```\n\n## 2. configure_extension_build 函数流程\n\n`configure_extension_build` 函数用于配置并生成构建和安装 PyTorch C/C++ 扩展模块所需要的关键参数：\n- `ext_modules`：列表，用于定义 C/C++ 扩展模块，以便在安装时通过 setuptools 构建和编译这些模块。\n- `cmdclass`: 字典，自定义命令，用于在安装过程中执行一些额外的操作（例如：编译其他资源、运行脚本等）。\n- `packages`: 列表，用于指定要包含在安装包中的 Python 包,每个元素是一个包的名称或包路径。\n- `entry_points`: 字典，通常用于指定可执行脚本或插件系统的入口点。\n- `extra_install_requires`: 字典，当想实现特定功能时，用此参数来定义**可选的额外依赖项**\n\nconfigure_extension_build 函数流程如下所示:\n\n```bash\nconfigure_extension_build()\n├─ 读取 CMake 缓存变量 (cmake_cache_vars)\n├─ 配置编译选项\n│  ├─ 从 CMakeCache.txt 或相关上下文中获取编译配置选项\n│  ├─ 根据操作系统设置编译/链接参数\n│  ├─ 处理调试/发布模式标志\n│  ├─ 处理 macOS 交叉编译选项\n│  └─ 定义辅助函数，生成传给链接器（linker）的 -rpath 参数\n├─ 声明扩展模块和包\n│  ├─ 排除不编译的目录 (caffe2/tools)\n│  ├─ 创建主扩展模块 (torch._C)\n│  └─ 条件添加 functorch 扩展\n├─ 定义自定义构建命令 (cmdclass)\n├─ 配置入口点 (console_scripts)\n└─ 返回配置参数\n```\n\n`rpath`（runtime search path）是一个在运行时指定动态库搜索路径的机制，让可执行文件或共享库知道去哪里寻找依赖的其它动态库。默认情况下，系统会在标准路径（如 /usr/lib, /usr/local/lib）或在由 `LD_LIBRARY_PATH`（或 `macOS` 的 `DYLD_LIBRARY_PATH`）等环境变量中指定的路径里查找；\n\nconfigure_extension_build 注释后的代码如下所示：\n\n```python\ndef configure_extension_build():\n    r\"\"\"\n    配置 PyTorch C/C++ 扩展构建选项，并返回给 setuptools.setup(...) 所需的部分参数。\n\n    Returns:\n      5 个元素组成的元组：\n        (extensions, cmdclass, packages, entry_points, extra_install_requires)\n    \"\"\"\n    # 从 CMakeCache.txt 或相关上下文中获取编译配置选项\n    cmake_cache_vars = get_cmake_cache_vars()\n\n    ################################################################################\n    # 1. 设置编译标志与链接标志的基础配置\n    ################################################################################\n    library_dirs = []\n    extra_install_requires = []\n\n    # 根据平台选择不同编译/链接选项\n    if IS_WINDOWS:\n        # /NODEFAULTLIB:LIBCMT.LIB - 指定只链接 DLL 版运行时\n        extra_link_args = [\"/NODEFAULTLIB:LIBCMT.LIB\"]\n        # /MD - 使用动态运行时库，/FS - 并行写PDB，/EHsc - 启用标准C++异常处理\n        extra_compile_args = [\"/MD\", \"/FS\", \"/EHsc\"]\n    else:\n        extra_link_args = []\n        extra_compile_args = [\n            \"-Wall\",\n            \"-Wextra\",\n            \"-Wno-strict-overflow\",\n            \"-Wno-unused-parameter\",\n            \"-Wno-missing-field-initializers\",\n            \"-Wno-unknown-pragmas\",\n            # -fno-strict-aliasing 可兼容早期 Python（2.6），避免代码对别名优化敏感\n            \"-fno-strict-aliasing\",\n        ]\n\n    # 将编译后生成的库路径加入 library_dirs，以便链接\n    library_dirs.append(lib_path)\n\n    main_compile_args = []\n    main_libraries = [\"torch_python\"]  # 默认链接到 torch_python 库\n    main_link_args = []\n    main_sources = [\"torch/csrc/stub.c\"]  # 默认源文件\n    \n    # 如果是只构建 libtorch wheel，则不需要 torch_python，而是链接 \"torch\"\n    # 并且 stub.c 也不使用\n    if BUILD_LIBTORCH_WHL:\n        main_libraries = [\"torch\"]\n        main_sources = []\n\n    # 处理 Debug / RelWithDebInfo 模式下的编译、链接标志\n    if build_type.is_debug():\n        if IS_WINDOWS:\n            extra_compile_args.append(\"/Z7\")      # 生成完整调试信息\n            extra_link_args.append(\"/DEBUG:FULL\") # 链接时也生成 pdb 等\n        else:\n            extra_compile_args += [\"-O0\", \"-g\"]   # 不优化并生成调试信息\n            extra_link_args += [\"-O0\", \"-g\"]\n    if build_type.is_rel_with_deb_info():\n        if IS_WINDOWS:\n            extra_compile_args.append(\"/Z7\")\n            extra_link_args.append(\"/DEBUG:FULL\")\n        else:\n            extra_compile_args += [\"-g\"]\n            extra_link_args += [\"-g\"]\n\n    # 额外 Python 依赖：若环境变量 PYTORCH_EXTRA_INSTALL_REQUIREMENTS 有值，就拆分并加入\n    pytorch_extra_install_requirements = os.getenv(\"PYTORCH_EXTRA_INSTALL_REQUIREMENTS\", \"\")\n    if pytorch_extra_install_requirements:\n        report(f\"pytorch_extra_install_requirements: {pytorch_extra_install_requirements}\")\n        extra_install_requires += pytorch_extra_install_requirements.split(\"|\")\n\n    # macOS 交叉编译配置：若 CMAKE_OSX_ARCHITECTURES 指定为 arm64 或 x86_64 等\n    if IS_DARWIN:\n        macos_target_arch = os.getenv(\"CMAKE_OSX_ARCHITECTURES\", \"\")\n        if macos_target_arch in [\"arm64\", \"x86_64\"]:\n            macos_sysroot_path = os.getenv(\"CMAKE_OSX_SYSROOT\")\n            if macos_sysroot_path is None:\n                # 调用 xcrun --show-sdk-path 来获取macOS的sysroot\n                macos_sysroot_path = (\n                    subprocess.check_output([\"xcrun\", \"--show-sdk-path\", \"--sdk\", \"macosx\"])\n                    .decode(\"utf-8\")\n                    .strip()\n                )\n            # 为编译/链接追加 -arch 和 -isysroot 参数，以匹配目标架构\n            extra_compile_args += [\n                \"-arch\", macos_target_arch,\n                \"-isysroot\", macos_sysroot_path,\n            ]\n            extra_link_args += [\"-arch\", macos_target_arch]\n\n    # 定义一个辅助函数，用于生成相对路径的 rpath 参数\n    def make_relative_rpath_args(path):\n        if IS_DARWIN:\n            # macOS 下可使用 @loader_path\n            return [f\"-Wl,-rpath,@loader_path/{path}\"]\n        elif IS_WINDOWS:\n            # Windows 下不使用 rpath\n            return []\n        else:\n            # Linux 下用 $ORIGIN\n            return [f\"-Wl,-rpath,$ORIGIN/{path}\"]\n\n    ################################################################################\n    # 2. 声明扩展模块和 Python 包\n    ################################################################################\n    extensions = []\n\n    # exclude 某些不需要作为 Python 包的目录\n    excludes = [\"tools\", \"tools.*\", \"caffe2\", \"caffe2.*\"]\n\n    # 若未开启 BUILD_FUNCTORCH，则排除 functorch 包\n    if not cmake_cache_vars[\"BUILD_FUNCTORCH\"]:\n        excludes.extend([\"functorch\", \"functorch.*\"])\n\n    # 使用 find_packages 排除上述目录，获取最终要打包的 Python 包列表\n    packages = find_packages(exclude=excludes)\n\n    # 定义主扩展模块： torch._C\n    C = Extension(\n        \"torch._C\",                             # 模块的全名, 安装后可通过 torch._C 导入核心\n        libraries=main_libraries,               # 链接所依赖的库名列表\n        sources=main_sources,                   # 源文件列表，编译这些文件生成模块\n        language=\"c\",                           # 指定语言，决定使用哪种编译器\n        extra_compile_args=main_compile_args + extra_compile_args, # 编译时传递的额外命令行参数\n        include_dirs=[],                        # 头文件搜索路径\n        library_dirs=library_dirs,              # 库文件搜索路径\n        extra_link_args=(                       # 链接时传递的额外命令行参数，通常用于设置 rpath 等\n            extra_link_args\n            + main_link_args\n            + make_relative_rpath_args(\"lib\")   # rpath 配置\n        ),\n    )\n    extensions.append(C)\n\n    # 如果编译 functorch，则再添加一个 functorch._C 占位扩展（由外部CMake完成实际构建并复制）\n    if cmake_cache_vars[\"BUILD_FUNCTORCH\"]:\n        extensions.append(\n            Extension(name=\"functorch._C\", sources=[]),\n        )\n\n    # 自定义命令类，用于替换默认的 setuptools build_ext、bdist_wheel 等\n    cmdclass = {\n        \"bdist_wheel\": wheel_concatenate,\n        \"build_ext\": build_ext,\n        \"clean\": clean,\n        \"install\": install,\n        \"sdist\": sdist,\n    }\n\n    # 定义 Python 的命令行入口点\n    entry_points = {\n        \"console_scripts\": [\n            \"torchrun = torch.distributed.run:main\",   # 安装后可使用 torchrun 命令\n        ],\n        \"torchrun.logs_specs\": [\n            \"default = torch.distributed.elastic.multiprocessing:DefaultLogsSpecs\",\n        ],\n    }\n\n    # 若编译开启分布式，则把 torchfrtrace 脚本也加入 console_scripts\n    if cmake_cache_vars[\"USE_DISTRIBUTED\"]:\n        entry_points[\"console_scripts\"].append(\n            \"torchfrtrace = tools.flight_recorder.fr_trace:main\",\n        )\n\n    # ----------------------------------------------------------------\n    # 在返回之前，使用 logger.info() 打印当前生成的各参数\n    # ----------------------------------------------------------------\n    \n    logger.info(\"=== configure_extension_build Results ===\")\n    logger.info(\"extensions: %s\", extensions)\n    logger.info(\"cmdclass: %s\", cmdclass)\n    logger.info(\"packages: %s\", packages)\n    logger.info(\"entry_points: %s\", entry_points)\n    logger.info(\"extra_install_requires: %s\", extra_install_requires)\n    logger.info(\"=========================================\")\n    \n    # 最后将上述配置统一返回\n    return extensions, cmdclass, packages, entry_points, extra_install_requires\n```\n\n上述代码看起来内容很多，其实最核心最本质的代码就是定义主拓展模块那部分代码：\n\n```python\n# 定义主扩展模块： torch._C\nC = Extension(\n    \"torch._C\",                             # 模块的全名, 安装后可通过 torch._C 导入\n    libraries=main_libraries,               # 链接所依赖的库名列表\n    sources=main_sources,                   # 源文件列表，编译这些文件生成模块\n    language=\"c\",                           # 指定语言，决定使用哪种编译器\n    extra_compile_args=main_compile_args + extra_compile_args, # 编译时传递的额外命令行参数\n    include_dirs=[],                        # 头文件搜索路径\n    library_dirs=library_dirs,              # 库文件搜索路径\n    extra_link_args=(                       # 链接时传递的额外命令行参数，通常用于设置 rpath 等\n        extra_link_args\n        + main_link_args\n        + make_relative_rpath_args(\"lib\")   # rpath 配置\n    ),\n)\nextensions.append(C)\n```\n\nlibrary_dirs 存放了编译后生成的库路径，其涉及到的代码如下所示：\n\n```python\nlib_path = os.path.join(cwd, \"torch\", \"lib\")\nlibrary_dirs.append(lib_path)\n```\n\n`pytorch/torch/lib` 目录主要用于**存放代码构建后生成的本地动态库（也称为共享库），这些库构成了 PyTorch 的核心 C++ 后端**。即这个目录的文件是用来实现张量计算、自动微分、设备管理、分布式训练等功能的二进制代码，它们通过 Python 的扩展机制（例如 _C 模块）与 Python 层进行连接和交互。\n\nextensions, cmdclass, packages, entry_points 打印后的内容如下所示:\n\n```bash\nINFO:__main__:=== configure_extension_build Results ===\nINFO:__main__:extensions: [<setuptools.extension.Extension('torch._C') at 0x100db26c0>, <setuptools.extension.Extension('functorch._C') at 0x100b2b0e0>]\nINFO:__main__:cmdclass: {'bdist_wheel': <class '__main__.wheel_concatenate'>, 'build_ext': <class '__main__.build_ext'>, 'clean': <class '__main__.clean'>, 'install': <class '__main__.install'>, 'sdist': <class '__main__.sdist'>}\nINFO:__main__:packages: ['torch', 'torchgen', 'functorch', 'torch.monitor', 'torch._dispatch', 'torch._custom_op', 'torch.nn', 'torch.mps', 'torch.onnx', 'torch.cpu', \\\n'torch.distributed', 'torch.autograd', 'torch.profiler', 'torch.sparse', 'torch._awaits', 'torch.export', 'torch.compiler', 'torch.distributions', 'torch.package', 'torch.func', \\\n'torch.nn.attention', 'torch.nn.parallel', 'torch.nn.qat', 'torch.nn.quantized', 'torch.nn.backends', 'torch.nn.utils', 'torch.nn.quantizable', 'torch.nn.intrinsic', 'torch.nn.modules','torch.utils.tensorboard', \\\n'torch.nn.qat.*', 'torch.nn.quantized.*', 'torch.nn.*', 'torch.fx.*, 'torch.utils.*']\nINFO:__main__:entry_points: {'console_scripts': ['torchrun = torch.distributed.run:main', 'torchfrtrace = tools.flight_recorder.fr_trace:main'], 'torchrun.logs_specs': ['default = torch.distributed.elastic.multiprocessing:DefaultLogsSpecs']}\nINFO:__main__:extra_install_requires: []\nINFO:__main__:=========================================\n```\n\n为了方便阅读 packages 内容手动做了简化，上述的内容是简化后的信息。\n- torch.fx.* 和 torch.nn.quantized.* 做了省略，表示 torch.fx 和 torch.nn.quantized 相关模块;\n- torch.nn.qat.* 表示训练中量化功能涉及到的相关模块。torch.nn.* 表示神经网络模块相关；\n- torch.utils.* 表示 torch 接口功能相关模块"
  },
  {
    "path": "2-deep_learning_basic/反向传播与梯度下降详解.md",
    "content": "- [一，前向传播与反向传播](#一前向传播与反向传播)\n  - [1.1，神经网络训练过程](#11神经网络训练过程)\n  - [1.2，前向传播](#12前向传播)\n  - [1.3，反向传播](#13反向传播)\n  - [1.4，总结](#14总结)\n- [二，梯度下降](#二梯度下降)\n  - [2.1，深度学习中的优化](#21深度学习中的优化)\n  - [2.2，如何理解梯度下降法](#22如何理解梯度下降法)\n  - [2.3，梯度下降原理](#23梯度下降原理)\n- [三，随机梯度下降与小批量随机梯度下降](#三随机梯度下降与小批量随机梯度下降)\n  - [3.1，随机梯度下降](#31随机梯度下降)\n  - [3.2，小批量随机梯度下降](#32小批量随机梯度下降)\n- [四，总结](#四总结)\n- [参考资料](#参考资料)\n\n## 一，前向传播与反向传播\n### 1.1，神经网络训练过程\n\n神经网络训练过程是：\n1. 先通过随机参数“猜“一个结果（模型前向传播过程），这里称为预测结果 $a$；\n2. 然后计算 $a$ 与样本标签值 $y$ 的差距（即损失函数的计算过程）；\n3. 随后通过反向传播算法更新神经元参数，使用新的参数再试一次，这一次就不是“猜”了，而是有依据地向正确的方向靠近，毕竟参数的调整是有策略的（基于梯度下降策略）。\n\n以上步骤如此反复多次，一直到预测结果和真实结果之间相差无几，亦即 $|a-y|\\rightarrow 0$，则训练结束。\n\n总结：**所谓模型训练，其实就是通过如 `SGD` 优化算法指导模型参数更新的过程**。\n\n### 1.2，前向传播\n\n前向传播(forward propagation 或 forward pass)指的是: 按顺序(从输入层到输出层)计算和存储神经网络中每层的结果。\n\n为了更深入理解前向传播的计算过程，我们可以根据网络结构绘制网络的前向传播计算图。下图是简单网络与对应的计算图示例:\n\n![网络结构与前向计算图](../images/bp/network_structure_and_forward_computation_graph.png)\n\n其中正方形表示变量，圆圈表示操作符。数据流的方向是从左到右依次计算。\n\n### 1.3，反向传播\n\n反向传播(`backward propagation`，简称 `BP`)指的是**计算神经网络参数梯度的方法**。其原理是基于微积分中的**链式规则**，按相反的顺序从输出层到输入层遍历网络，依次计算每个中间变量和参数的梯度。\n> 梯度的自动计算(自动微分)大大简化了深度学习算法的实现。\n\n注意，反向传播算法会重复利用前向传播中存储的中间值，以避免重复计算，因此，需要保留前向传播的中间结果，这也会导致模型训练比单纯的预测需要更多的内存（显存）。同时这些中间结果占用内存（显存）大小与网络层的数量和批量（`batch_size`）大小成正比，因此使用大 `batch_size` 训练更深层次的网络更容易导致内存不足（out of memory）的错误！\n\n### 1.4，总结\n\n- 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量，它的顺序是从输入层到输出层。 \n- 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。\n- 在训练神经网络时，在初始化模型参数后，我们交替使用前向传播和反向传播，基于反向传播计算得到的梯度，结合随机梯度下降优化算法（或者 `Adam` 等其他优化算法）来更新模型参数。\n- 深度学习模型训练比预测需要更多的内存。\n\n## 二，梯度下降\n\n### 2.1，深度学习中的优化\n\n大多数深度学习算法都涉及某种形式的优化。**优化器的目的是更新网络权重参数，使得我们平滑地到达损失面中损失值的最小点**。\n\n深度学习优化存在许多挑战。其中一些最令人烦恼的是局部最小值、鞍点和梯度消失。\n\n- **局部最小值**(`local minimum`): 对于任何目标函数 $f(x)$，如果在 $x$ 处对应的 $f(x)$ 值小于在 $x$ 附近任何其他点的 $f(x)$ 值，那么 $f(x)$ 可能是局部最小值。如果 $f(x)$ 在 $x$ 处的值是整个域上目标函数的最小值，那么 $f(x)$ 是全局最小值。\n- **鞍点**(`saddle point`): 指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。\n- **梯度消失**(`vanishing gradient`): 因为某些原因导致**目标函数** $f$ 的梯度接近零（即梯度消失问题），是在引入 `ReLU` 激活函数和 `ResNet` 之前训练深度学习模型相当棘手的原因之一。\n\n在深度学习中，大多数目标函数都很复杂，没有解析解，因此，我们需使用数值优化算法，本文中的优化算法: `SGD` 和 `Adam` 都属于此类别。\n\n### 2.2，如何理解梯度下降法\n\n梯度下降（`gradient descent`, `GD`）算法是神经网络模型训练中最为常见的优化器。尽管梯度下降(`gradient descent`)很少直接用于深度学习，但理解它是理解**随机梯度下降和小批量随机梯度下降**算法的基础。\n\n大多数文章都是以“一个人被困在山上，需要迅速下到谷底”来举例理解梯度下降法，但这并不完全准确。在自然界中，梯度下降的最好例子，就是泉水下山的过程：\n\n1. 水受重力影响，会在当前位置，沿着**最陡峭**的方向流动，有时会形成瀑布（**梯度的反方向为函数值下降最快的方向**）；\n2. 水流下山的路径不是唯一的，在同一个地点，有可能有多个位置具有同样的陡峭程度，而造成了分流（可以得到多个解）；\n3. 遇到坑洼地区，有可能形成湖泊，而终止下山过程（不能得到全局最优解，而是局部最优解）。\n\n> 示例参考 [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%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/02.3-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D.html)。\n\n### 2.3，梯度下降原理\n\n梯度下降的数学公式：\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\nabla J(\\theta) \\tag{1}\n$$\n\n其中：\n\n- $\\theta_{n+1}$：下一个值（神经网络中参数更新后的值）；\n- $\\theta_n$：当前值（当前网络参数值）；\n- $-$：减号，梯度的反向（梯度的反方向为函数值下降最快的方向）；\n- $\\eta$：学习率或步长，控制每一步走的距离，不要太快以免错过了最佳景点，不要太慢以免时间太长（需要手动调整的超参数）；\n- $\\nabla$：**梯度**，，表示函数当前位置的最快上升点（梯度向量指向上坡，负梯度向量指向下坡）；\n- $J(\\theta)$：函数（等待优化的目标函数）。\n\n下图展示了梯度下降法的步骤。梯度下降的目的就是使得 $x$ 值向极值点逼近。\n\n![梯度下降的步骤](../images/bp/gd_concept.png)\n\n下面我通过一个简单的双变量凸函数 $J(x, y) = x^2 + 2y^2$ 为例，来描述梯度下降的优化过程。\n\n通过梯度下降法寻找函数的最小值，首先得计算其函数梯度:\n\n$$\n{\\partial{J(x,y)} \\over \\partial{x}} = 2x \\\\\n{\\partial{J(x,y)} \\over \\partial{y}} = 4y\n$$\n\n设初始点为 $(x_0, y_0) = (-3, -3)$，学习率 $\\eta = 0.1$，根据梯度下降公式(1)，可得参数迭代过程的计算公式:\n\n$$\n\\begin{align} \n(x_{n+1}, y_{n+1}) &= (x_n, y_n) - \\eta \\cdot \\nabla J(x, y) \\nonumber \\\\\n&= (x_n, y_n) - \\eta \\cdot (2x, 4y) \\tag{2}\n\\end{align}\n$$\n\n这里手动计算下下一个迭代点的值:\n\n$$\n\\begin{aligned}\n(x_1, y_1) &= (-3, -3) - 0.1*(2*-3, 4*-3) \\\\\n&= (-3 + 0.6, -3 + 1.2) \\\\\n&= (-2.4, -1.8) \\\\\n\\end{aligned}\n$$\n\n根据上述公式 (2)，假设终止条件为 $J(x,y)$ < 0. 005，迭代过程如下表1所示。\n\n表1 双变量函数梯度下降的迭代过程\n\n|迭代次数|$x$|$y$|$J(x,y)$|\n|---|---|---|---|\n|1|-3|-3|27|\n|2|-2.4|y=-1.8|12.24|\n|...|...|...|...|\n|16|-0.084442|-0.000846|0.007132|\n|17|-0.067554|y=-0.000508|0.004564|\n\n迭代 $17$ 次后，$J(x,y)$ 的值为 $0.004564$，满足小于 $0.005$ 的条件，停止迭代。\n\n由于是双变量，所以梯度下降的迭代过程需要用三维图来解释。表2可视化了三维空间内的梯度下降过程。\n\n|观察角度1|观察角度2|\n|-------|--------|\n|<img src=\"../images/bp/gd_double_variable1.png\">|<img src=\"../images/bp/gd_double_variable2.png\">|\n\n图中间那条隐隐的黑色线，表示梯度下降的过程，从红色的高地一直沿着坡度向下走，直到蓝色的洼地。\n\n双变量凸函数 $J(x, y) = x^2 + 2y^2$ 的梯度下降优化过程以及可视化代码如下所示:\n\n```python\n# Copyright (c) Microsoft. All rights reserved.\n# Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\ndef target_function(x,y):\n    J = pow(x, 2) + 2*pow(y, 2)\n    return J\n\ndef derivative_function(theta):\n    x = theta[0]\n    y = theta[1]\n    return np.array([2*x, 4*y])\n\ndef show_3d_surface(x, y, z):\n    fig = plt.figure()\n    ax = Axes3D(fig)\n    u = np.linspace(-3, 3, 100)\n    v = np.linspace(-3, 3, 100)\n    X, Y = np.meshgrid(u, v)\n    R = np.zeros((len(u), len(v)))\n    for i in range(len(u)):\n        for j in range(len(v)):\n            R[i, j] = pow(X[i, j], 2)+ 4*pow(Y[i, j], 2)\n\n    ax.plot_surface(X, Y, R, cmap='rainbow')\n    plt.plot(x, y, z, c='black', linewidth=1.5,  marker='o', linestyle='solid')\n    plt.show()\n\nif __name__ == '__main__':\n    theta = np.array([-3, -3]) # 输入为双变量\n    eta = 0.1 # 学习率\n    error = 5e-3 # 迭代终止条件，目标函数值 < error\n    X = []\n    Y = []\n    Z = []\n    for i in range(50):\n        print(theta)\n        x = theta[0]\n        y = theta[1]\n        z = target_function(x,y)\n        X.append(x)\n        Y.append(y)\n        Z.append(z)\n        print(\"%d: x=%f, y=%f, z=%f\" % (i,x,y,z))\n        d_theta = derivative_function(theta)\n        print(\"    \", d_theta)\n        theta = theta - eta * d_theta\n        if z < error:\n            break\n    show_3d_surface(X,Y,Z)\n```\n\n注意！总结下，不同的步长 $\\eta$ ，随着迭代次数的增加，会导致被优化函数 $J$ 的值有不同的变化：\n\n![different_lr_loss](../images/bp/different_lr_loss.png)\n> 图片来源[如何理解梯度下降法？](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)。\n\n## 三，随机梯度下降与小批量随机梯度下降\n\n### 3.1，随机梯度下降\n\n在深度学习中，目标函数通常是训练数据集中每个样本的损失函数的平均值。如果使用梯度下降法，则每个自变量迭代的计算代价为 $O(n)$，它随 $n$（样本数目）线性增⻓。因此，当训练数据集较大时，每次迭代的梯度下降计算代价将较高。\n\n随机梯度下降(`SGD`)可降低每次迭代时的计算代价。在随机梯度下降的每次迭代中，我们对数据样本**随机均匀**采样一个索引 $i$，其中 $i \\in {1, . . . , n}$，并计算梯度 $\\nabla J(\\theta)$ 以更新权重参数 $\\theta$:\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\nabla J_i(\\theta) \\tag{3}\n$$\n\n每次迭代的计算代价从梯度下降的 $O(n)$ 降至常数 $O(1)$。另外，值得强调的是，随机梯度 $\\nabla J_i(\\theta)$ 是对完整梯度 $\\nabla J(\\theta)$ 的无偏估计。\n> 无偏估计是用样本统计量来估计总体参数时的一种无偏推断。 \n\n在实际应用中，**随机梯度下降 SGD 法必须和动态学习率**方法结合起来使用，否则使用固定学习率 + SGD的组合会使得模型收敛过程变得更复杂。学习率的调整策略可参考我之前写的文章-[深度学习炼丹-超参数设定和模型训练](https://github.com/HarleysZhang/deep_learning_alchemy/blob/main/3-deep_learning_alchemy/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%82%BC%E4%B8%B9-%E8%B6%85%E5%8F%82%E6%95%B0%E8%AE%BE%E5%AE%9A%E5%92%8C%E6%A8%A1%E5%9E%8B%E8%AE%AD%E7%BB%83.md)。\n\n### 3.2，小批量随机梯度下降\n\n前面讲的梯度下降（`GD`）和随机梯度下降（`SGD`）方法都过于极端，要么使用完整数据集来计算梯度并更新参数，要么一次只处理一个训练样本来更新参数。在实际项目中，会对两者取折中，即小批量随机梯度下降(`minibatch stochastic gradient descent`)，其有以下优点:\n\n- 首先，小批量损失的梯度是对训练集梯度的估计，其质量随着批量大小的增加而提高。\n\n- 其次，由于现代计算平台提供的**并行性**，批量计算比单个示例的 $m$ 计算效率更高。\n\n小批量的所有样本数据元素都是从训练集中**随机抽取**的，假设样本数目个数为 $m$（`batch_size = m`），即迭代训练的每一步我们都考虑的是一个大小为 $m$ 的小批量样本 $\\mathbf{x_{1...m}}$。\n\n$$\n\\theta_{n+1} = \\theta_{n} - \\eta \\cdot \\frac{1}{m}\\nabla \\sum_{i}^{m}J_i(\\textrm{x},\\theta) \\tag{3}\n$$\n\n一般项目中使用 `SGD` 优化算法都默认会使用小批量随机梯度下降，即 $m > 1$，除非显卡显存不够了，才会设置 $m = 1$。\n\n> 梯度计算符号也可用 $\\frac{\\partial\\ell(\\textrm{x}_i, \\theta)}{\\partial\\theta}$ 表示。其中 $\\textrm{x}$ 表示训练集，$\\theta$ 表示网络参数，$\\ell(\\textrm{x}_i, \\theta)$ 表示待优化的目标函数。\n\n## 四，总结\n\n虽然随机梯度下降法（`SGD`）简单有效，但它需要仔细调整模型超参数，特别是优化中使用的**学习率**，以及**模型参数的初始值**。 由于每一层的输入都受到前面所有层的参数的影响，因此训练变得很复杂。（因为随着网络变得更深，网络参数的微小变化会被累积和放大）\n\n## 参考资料\n\n1. [如何理解梯度下降法？](https://mp.weixin.qq.com/s/SlTV6lbPnauf36bZLXglCw)\n2. [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%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/02.3-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D.html)\n3. 《动手学习深度学习11章-优化算法》"
  },
  {
    "path": "2-deep_learning_basic/深度学习基础-优化算法详解.md",
    "content": "## 目录\n- [目录](#目录)\n- [前言](#前言)\n- [一，梯度下降优化算法](#一梯度下降优化算法)\n  - [1.1，随机梯度下降 SGD](#11随机梯度下降-sgd)\n  - [1.2，动量 Momentum](#12动量-momentum)\n  - [1.3，Nesterov 动量](#13nesterov-动量)\n  - [1.4，代码实践](#14代码实践)\n- [二，自适应学习率算法](#二自适应学习率算法)\n  - [2.1，AdaGrad](#21adagrad)\n  - [2.2，RMSProp](#22rmsprop)\n  - [2.3，AdaDelta](#23adadelta)\n  - [2.4，Adam](#24adam)\n- [三，总结](#三总结)\n  - [3.1，PyTorch 的十个优化器](#31pytorch-的十个优化器)\n  - [3.2，优化算法总结](#32优化算法总结)\n- [参考资料](#参考资料)\n\n## 前言\n\n所谓深度神经网络的优化算法，**即用来更新神经网络参数，并使损失函数最小化的算法**。优化算法对于深度学习非常重要，如果说网络参数初始化（模型迭代的初始点）能够决定模型是否收敛，那优化算法的性能则**直接**影响模型的训练效率。\n\n了解不同优化算法的原理及其超参数的作用将使我们更有效的调整优化器的超参数，从而提高模型的性能。\n\n本文的优化算法特指: 寻找神经网络上的一组参数 $\\theta $，它能显著地降低损失函数 $J(\\theta )$，该损失函数通常包括整个训练集上的性能评估和额外的正则化项。\n\n> 本文损失函数、目标函数、代价函数不严格区分定义。\n\n## 一，梯度下降优化算法\n\n### 1.1，随机梯度下降 SGD\n\n梯度下降法是最基本的一类优化器，目前主要分为三种梯度下降法：标准梯度下降法(GD, Gradient Descent)，随机梯度下降法(SGD, Stochastic Gradient Descent)及批量梯度下降法(BGD, Batch Gradient Descent)。\n\n深度学习项目中的 `SGD` 优化一般默认指批量梯度下降法。其算法描述如下:\n\n- 输入和超参数: $\\eta$ 全局学习率\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 更新参数：$\\theta_t = \\theta_{t-1} - \\eta \\cdot g_t$\n\nSGD 优化算法是最经典的神经网络优化方法，**虽然收敛速度慢，但是收敛效果比较稳定**。\n\n下图1展现了随机梯度下降算法的梯度搜索轨迹示意图。可以看出由于梯度的随机性质，梯度搜索轨迹要很嘈杂（**动荡现象**）。\n\n![sgd_algorithm](../images/optimizers/sgd_algorithm.png)\n\n因此，在实际应用中，**随机梯度下降 SGD 法必须和动态学习率**方法结合起来使用，否则使用固定学习率 + SGD的组合会使得模型收敛过程变得更复杂。\n\n### 1.2，动量 Momentum\n\n虽然**随机梯度下降**仍然是非常受欢迎的优化方法，但其学习过程有时会很慢且其梯度更新方向完全依赖于当前 `batch` 样本数据计算出的梯度，因而十分不稳定，因为数据可能有噪音。\n\n受启发于物理学研究领域研究，基于动量 Momentum  (Polyak, 1964) 的 SGD 算法**用于改善参数更新时可能产生的振荡现象**。动量算法旨在加速学习，特别是处理高曲率、小但一致的梯度，或是带噪声的梯度。两种算法效果对比如下图 2所示。\n\n> 花书中对动量算法对目的解释是，解决两个问题: Hessian 矩阵的病态条件和随机梯度的方差。更偏学术化一点。\n\n![动量算法效果](../images/optimizers/momentum_algorithm.png)\n\nMomentum 算法的通俗理解就是，其模拟了物体运动时的惯性，即更新参数的时候会同时结合过去以及当前 batch 的梯度。算法在更新的时候会一定程度上保留之前更新的方向，同时利用当前 batch 的梯度微调最终的更新方向。这样一来，可以在一定程度上增加稳定性，从而学习地更快，并且还有一定摆脱局部最优的能力。\n\n下图3展现了动量算法的前进方向。\n\n![动量算法的前进方向](../images/optimizers/momentum_algorithm_update.png)\n\n第一次的梯度更新完毕后，会记录 $v1$ 的动量值。在“求梯度点”进行第二次梯度检查时，得到2号方向，与 $v1$ 的动量**组合**后，最终的更新为 2' 方向。这样一来，由于有 $v1$ 的存在，会迫使梯度更新方向具备“惯性”，从而可以减小随机样本造成的震荡。\n\n**Momentum 算法描述如下**:\n\n1，**输入和参数**:\n\n- $\\eta$ - 全局学习率\n- $\\alpha$ - 动量参数，一般取值为 0.5, 0.9, 0.99，取 `0`，则等效于常规的随机梯度下降法，其**控制动量信息对整体梯度更新的影响程度**。\n- $v_t$ - 当前时刻的动量，初值为 0。\n\n2，**算法计算过程**：\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 计算速度更新：$v_t = \\alpha \\cdot v_{t-1} + \\eta \\cdot g_t$ (公式1)\n\n- 更新参数：$\\theta_t = \\theta_{t-1} - v_t$ (公式2)\n\n> 注意，这里的公式1和公式2和花书上的公式形式上略有不同，但其最终结果是相同的。本文给出的手工推导迭代公式，来源文章 [15.2 梯度下降优化算法](https://github.com/microsoft/ai-edu/blob/master/%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.2-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E4%BC%98%E5%8C%96%E7%AE%97%E6%B3%95.md)。\n\n通过推导参数更新迭代公式，更容易理解算法，根据算法公式(1)(2)，以$W$参数为例，有：\n\n1. $v_0 = 0$\n2. $dW_0 = \\nabla J(w)$\n3. $v_1 = \\alpha v_0 + \\eta \\cdot dW_0 = \\eta \\cdot dW_0$\n4. $W_1 = W_0 - v_1=W_0 - \\eta \\cdot dW_0$\n5. $dW_1 = \\nabla J(w)$\n6. $v_2 = \\alpha v_1 + \\eta dW_1$\n7. $W_2 = W_1 - v_2 = W_1 - (\\alpha v_1 +\\eta dW_1) = W_1 - \\alpha \\cdot \\eta \\cdot dW_0 - \\eta \\cdot dW_1$\n8. $dW_2 = \\nabla J(w)$\n9. $v_3=\\alpha v_2 + \\eta dW_2$\n10. $W_3 = W_2 - v_3=W_2-(\\alpha v_2 + \\eta dW_2) = W_2 - \\alpha^2 \\eta dW_0 - \\alpha \\eta dW_1 - \\eta dW_2$\n\n可以看出与普通 SGD 的算法 $W_3 = W_2 - \\eta dW_2$ 相比，动量法不但每次要减去当前梯度，还要减去历史梯度$W_0, W_1$ 乘以一个不断减弱的因子$\\alpha$、$\\alpha^2$，因为$\\alpha$小于1，所以$\\alpha^2$比$\\alpha$小，$\\alpha^3$比$\\alpha^2$小。这种方式的学名叫做**指数加权平均**。\n\n在实际模型训练中，SGD 和动量法的比较如下表所示。\n\n> 实验效果对比图来源于资料 1。\n\n|算法     | 损失函数和准确率|\n|--------|------------------------------------------------------------|\n|SGD     |![SGD训练损失函数曲线](../images/optimizers/op_sgd_train_loss_log.png)|\n|Momentum|![momentum训练损失函数曲线](../images/optimizers/op_momentum_train_loss_log.png)|\n\n从上表的对比可以看出，同一个深度模型， 普通随机梯度下降法的曲线震荡很严重，且经过 epoch=10000 次也没有到达预定 0.001 的损失值；但动量算法经过 2000 个 epoch 就迭代结束。\n\n### 1.3，Nesterov 动量\n\n受 Nesterov 加速梯度算法 (Nesterov, 1983, 2004) 启发，Sutskever *et al.* (2013) 提出了动量算法的一个变种，Nesterov 动量随机下降法（`NAG` ，英文全称是 Nesterov Accelerated Gradient，或者叫做 Nesterov Momentum 。\n\nNesterov 动量随机梯度下降方法是在上述动量梯度下降法更新梯度时加入对当前梯度的校正，简单解释就是**往标准动量方法中添加了一个校正因子**。\n\nNAG 算法描述如下:\n\n1，**输入和参数**：\n\n- $\\eta$ - 全局学习率\n- $\\alpha$ - 动量参数，缺省取值 0.9\n- $v$ - 动量，初始值为0\n\n2，**算法计算过程**：\n\n- 参数临时更新：$\\hat \\theta = \\theta_{t-1} - \\alpha \\cdot v_{t-1}$\n- 网络前向传播计算：$f(\\hat \\theta)$\n- 计算梯度：$g_t = \\nabla_{\\hat\\theta} J(\\hat \\theta)$\n- 计算速度更新：$v_t = \\alpha \\cdot v_{t-1} + \\eta \\cdot g_t$\n- 更新参数：$\\theta_t = \\theta_{t-1}  - v_t$\n\n### 1.4，代码实践\n\nPytorch 框架中把普通 SGD、Momentum 算法和 Nesterov Momentum 算法的实现结合在一起了，对应的类是 `torch.optim.SGD`。注意，其更新公式与其他框架略有不同，其中 $p$、$g$、$v$、$\\mu$ 表示分别是参数、梯度、速度和动量。\n\n$$\n\\begin{align}\nv_{t+1} &= \\mu \\cdot v_{t} + g_{t+1} \\nonumber \\\\ \np_{t+1} &= p_{t} - \\text{lr} \\cdot v_{t+1} \\nonumber \\\\\n&= p_{t} - \\text{lr} \\cdot \\mu \\cdot v_{t} - \\text{lr} \\cdot g_{t+1} \\nonumber\n\\end{align}\n$$\n\n```python\n# 和源码比省略了部分不常用参数\nclass torch.optim.SGD(params, lr=required, momentum=0, dampening=0,\n                 weight_decay=0, nesterov=False)\n```\n\n1，**功能解释**：\n\n可实现 SGD 优化算法、带动量 SGD 优化算法、带 NAG(Nesterov accelerated gradient)动量 SGD 优化算法，并且均可拥有 weight_decay 项。\n\n2，**参数解释**：\n\n- `params`(iterable): 参数组(参数组的概念参考优化器基类: `Optimizer`)，即优化器要管理的那部分参数。\n\n- `lr`(float): 初始学习率，可按需随着训练过程不断调整学习率。\n- `momentum`(float): 动量因子，通常设置为 0.9，0.8。\n- `weight_decay`(float): 权值衰减系数，也就是 L2 正则项的系数。\n- `nesterov`(bool)- bool 选项，是否使用 NAG(Nesterov accelerated gradient)。\n\n训练模型时常用配置如下:\n\n```python\ntorch.optim.SGD(lr=0.02, momentum=0.9, weight_decay=0.0001)\n```\n\n## 二，自适应学习率算法\n\n神经网络研究员早就意识到学习率肯定是难以设置的超参数之一，因为它对深度学习模型的性能有着显著的影响。\n\n### 2.1，AdaGrad\n\n在 AdaGrad (Duchi et al., 2011) 提出之前，我们对于所有的参数使用相同的学习率进行更新，它是第一个自适应学习率算法，通过**所有梯度历史平方值之和的平方根**，从而使得步长单调递减。它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率，从而避免统一的学习率难以适应所有维度的问题。\n\n`AdaGrad` 法根据训练轮数的不同，对学习率进行了动态调整。具体表现在，对低频出现的参数进行大的更新（快速下降的学习率），对高频出现的参数进行小的更新（相对较小的下降学习率）。因此，他很适合于处理稀疏数据。\n\nAdaGrad 算法描述如下:\n\n1，**输入和参数**\n\n- $\\eta$ - 全局学习率\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为`1e-6`\n- $r=0$ 初始值\n  \n\n**2，算法计算过程**：\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 累计平方梯度：$r_t = r_{t-1} + g_t \\odot g_t$\n\n- 计算梯度更新：$\\Delta \\theta = {\\eta \\over \\epsilon + \\sqrt{r_t}} \\odot g_t$(和动手学深度学习给出的学习率调整公式形式不同)\n\n- 更新参数：$\\theta_t=\\theta_{t-1} - \\Delta \\theta$\n\n$\\odot$ 按元素相乘，开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素都分别拥有自己的学习率。\n\n**AdaGrad 总结**：在凸优化背景中，AdaGrad 算法具有一些令人满意的理论性质。但是，经验上已经发现，对于训练深度神经网络模型而言，从训练开始时积累梯度平方会导致有效学习率过早和过量的减小。**AdaGrad 在某些深度学习模型上效果不错，但不是全部**。\n\nPytorch 框架中 AdaGrad 优化器:\n\n```python\nclass torch.optim.Adagrad(params, lr=0.01, lr_decay=0, weight_decay=0, initial _accumulator_value=0)\n```\n\n### 2.2，RMSProp\n\n`RMSProp`（Root Mean Square Prop），均方根反向传播。\n\nRMSProp 算法 (Hinton, 2012) 和 AdaGrad 算法的不同在于， **RMSProp算法使⽤了小批量随机梯度按元素平⽅的指数加权移动平均来调整学习率**。\n\nRMSProp 算法描述如下:\n\n1，**输入和参数**：\n\n- $\\eta$ - 全局学习率，建议设置为0.001\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为1e-8\n- $\\alpha$ - 衰减速率，建议缺省取值0.9\n- $r$ - 累积变量矩阵，与$\\theta$尺寸相同，初始化为0\n  \n\n2，**算法计算过程**（计算梯度和更新参数公式和 AdaGrad 算法一样）：\n\n- 累计平方梯度：$r = \\alpha \\cdot r + (1-\\alpha)(g_t \\odot g_t)$\n\n- 计算梯度更新：$\\Delta \\theta = {\\eta \\over \\sqrt{r + \\epsilon}} \\odot g_t$\n\n**RMSProp 总结**：经验上，RMSProp 已被证明是一种有效且实用的深度神经网络优化算法。目前，它是深度学习从业者经常采用的优化方法之一。其初始学习率设置为 `0.01` 时比较理想。\n\nPytorch 框架中 RMSprop 优化器:\n\n```python\nclass torch.optim.RMSprop(params, lr=0.01, alpha=0.99, eps=1e- 08, weight_decay=0, momentum=0, centered=False)\n```\n\n### 2.3，AdaDelta\n\nAdaDelta 法也是对 AdaGrad 法的一种改进，它**旨在解决深度模型训练后期，学习率过小问题**。相比计算之前所有梯度值的平方和，AdaDelta 法仅计算在一个大小为 $w$ 的时间区间内梯度值的累积和。\n\nRMSProp 算法计算过程如下:\n\n1，**参数定义**：\n\n- **Adadelta 没有学习率参数**。相反，它使用参数本身的变化率来调整学习率。\n\n- $s$ \\- 累积变量，初始值 0\n\n2，**算法计算过程**:\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n- 累积平方梯度：$s_t = \\alpha \\cdot s_{t-1} + (1-\\alpha) \\cdot g_t \\odot g_t$\n- 计算梯度更新：$\\Delta \\theta = \\sqrt{r_{t-1} + \\epsilon \\over s_t + \\epsilon} \\odot g_t$\n- 更新梯度：$\\theta_t = \\theta_{t-1} - \\Delta \\theta$\n- 更新变化量：$r = \\alpha \\cdot r_{t-1} + (1-\\alpha) \\cdot \\Delta \\theta \\odot \\Delta \\theta$\n\nPytorch 框架中 Adadelta 优化器:\n\n```python\nclass torch.optim.Adadelta(params, lr=1.0, rho=0.9, eps=1e- 06, weight_decay=0)\n```\n\n### 2.4，Adam\n\n**Adam** (Adaptive Moment Estimation，Kingma and Ba, 2014) 是另一种学习率自适应的优化算法，相当于 `RMSProp + Momentum` 的效果，即**动量项的 RMSprop 算法**。\n\nAdam 算法在 RMSProp 算法基础上对小批量随机梯度也做了指数加权移动平均。**和 AdaGrad 算法、RMSProp 算法以及 AdaDelta 算法一样，目标函数自变量中每个元素都分别拥有自己的学习率**。\n\nAdam 算法计算过程如下:\n\n1，**参数定义**：\n\n- $t$ - 当前迭代次数\n- $\\eta$ - 全局学习率，建议缺省值为0.001\n- $\\epsilon$ - 用于数值稳定的小常数，建议缺省值为1e-8\n- $\\beta_1, \\beta_2$ - 矩估计的指数衰减速率，$\\in[0,1)$，建议缺省值分别为0.9和0.999\n\n2，**算法计算过程**:\n\n- 计算梯度：$g_t = \\nabla_\\theta J(\\theta_{t-1})$\n\n- 计数器加一：$t=t+1$\n- 更新有偏一阶矩估计：$m_t = \\beta_1 \\cdot m_{t-1} + (1-\\beta_1) \\cdot g_t$\n- 更新有偏二阶矩估计：$v_t = \\beta_2 \\cdot v_{t-1} + (1-\\beta_2)(g_t \\odot g_t)$\n- 修正一阶矩的偏差：$\\hat m_t = m_t / (1-\\beta_1^t)$\n- 修正二阶矩的偏差：$\\hat v_t = v_t / (1-\\beta_2^t)$\n- 计算梯度更新：$\\Delta \\theta = \\eta \\cdot \\hat m_t /(\\epsilon + \\sqrt{\\hat v_t})$\n- 更新参数：$\\theta_t=\\theta_{t-1} - \\Delta \\theta$\n\n从上述公式可以看出 Adam 使用**指数加权移动平均值**来估算梯度的动量和二次矩，即使用了状态变量 $m_t、v_t$。\n\n**怎么理解 Adam 算法？**\n\n首先，在 Adam 中，动量直接并入了梯度一阶矩(指数加权)的估计。将动量加入 RMSProp 最直观的方法是将动量应用于缩放后的梯度。结合缩放的动量使用没有明确的理论动机。其次，Adam 包括偏置修正，修正从原点初始化的一阶矩(动量项)和(非中心的)二阶矩的估计。RMSProp 也采用了(非中心的)二阶矩估计，然而缺失了修正因子。因此，不像 Adam，RMSProp 二阶矩估计可能在训练初期有很高的偏置。 Adam 通常被认为对超参数的选择相当鲁棒，尽管学习率有时需要从建议的默认修改。\n\n> 初学者看看公式就行，这段话我也是摘抄花书，目前没有很深入理解。\n\nAdam 算法实现步骤如下。\n\n![Adam 算法实现步骤](../images/optimizers/adam_algorithm.png)\n\n**Adam 总结**：由于 Adam 继承了 RMSProp 的传统，所以学习率同样不宜设置太高，初始学习率设置为 `0.01` 时比较理想。\n\nPytorch 框架中 Adam 优化器:\n\n```python\nclass torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e- 08, weight_decay=0, amsgrad=False)\n```\n\n## 三，总结\n\n### 3.1，PyTorch 的十个优化器\n\nPyTorch 中所有的优化器基类是 `Optimizer` 类，在 Optimizer 类中定义了 5 个 实用的基本方法，分别是 \n\n- `zero_grad()`：将梯度清零。\n-  `step(closure)`：执行一步权重参数值更新, 其中可传入参数 closure(一个闭包)。\n- `state_dict()`：获取模型当前参数，以一个有序字典形式返回，key 是参数名，value 是参数。\n- `load_state_dict(state_dict)` ：将 state_dict 中的参数加载到当前网络，常用于 finetune。\n-  `add_param_group(param_group)`：给 optimizer 管理的参数组中增加一组参数，可为该组参数定制 lr, momentum, weight_decay 等，在 finetune 中常用。\n\nPyTorch 基于 `Optimizer` 基类构建了十种优化器，有常见的 SGD、ASGD、Rprop、 RMSprop、Adam 等等。注意 PyTorch 中的优化器和前文描述的优化算法略有不同，PyTorch 中 给出的优化器与原始论文中提出的优化方法，多少是有些改动的，详情可直接阅读源码。\n\n### 3.2，优化算法总结\n\n- Adam 等自适应学习率算法对于稀疏数据具有优势，且收敛速度很快；但精调参数的 SGD（+Momentum）往往能够取得更好的最终结果。\n\n- 用相同数量的超参数来调参，尽管有时自适应优化算法在训练集上的 loss 更小，但是他们在测试集上的 loss 却可能比 SGD 系列方法高。\n\n- 自适应优化算法在训练前期阶段在训练集上收敛的更快，但可能在测试集上的泛化性不好。 \n- 目前，最流行并且使用很高的优化算法包括 SGD、具动量的 SGD、RMSProp、 具动量的 RMSProp、AdaDelta 和 Adam。但选择哪一个算法主要取决于使用者对算法的熟悉程度(更方便调节超参数)。\n\n## 参考资料\n\n1. [《智能之门-神经网络与深度学习入门》-15.2 梯度下降优化算法](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.2-%E6%A2%AF%E5%BA%A6%E4%B8%8B%E9%99%8D%E4%BC%98%E5%8C%96%E7%AE%97%E6%B3%95.html)\n2. 《深度学习》-第八章 深度模型中的优化\n3. 《动手学深度学习》-优化算法\n"
  },
  {
    "path": "2-deep_learning_basic/深度学习基础-参数初始化详解.md",
    "content": "- [一，参数初始化概述](#一参数初始化概述)\n  - [1.1，进行网络参数初始化的原因](#11进行网络参数初始化的原因)\n  - [1.2，网络参数初始化为什么重要](#12网络参数初始化为什么重要)\n- [二，权重初始化方式分类](#二权重初始化方式分类)\n  - [2.1，全零初始化](#21全零初始化)\n  - [2.2，标准初始化](#22标准初始化)\n  - [2.3，Xavier 初始化](#23xavier-初始化)\n  - [2.4，He 初始化](#24he-初始化)\n  - [2.5，总结](#25总结)\n- [参考资料](#参考资料)\n\n> 本文内容参考资料为《智能之门-神经网络与深度学习入门》和《解析卷积神经网络》两本书，以及部分网络资料，加以个人理解和内容提炼总结得到，文中所有直方图的图片来源于参考资料3。\n\n## 一，参数初始化概述\n\n我们知道神经网络模型一般是依靠**随机梯度下降**优化算法进行神经网络参数更新的，而神经网络参数学习是非凸问题，利用梯度下降算法优化参数时，**网络权重参数的初始值选取十分关键**。\n\n首先得明确的是现代的**网络参数初始化策略**是简单的、启发式的。设定改进的初始化策略是一项困难的 任务，因为神经网络优化至今还未被很好地理解（即模型训练过程是一个黑盒）。\n\n大多数初始化策略基于在神经网络初始化时实现一些很好的性质。然而，我们并没有很好地理解这些性质中的哪些会在学习开始进行后的哪些情况下得以保持。进一步的难点是，有些初始点从优化的观点看或许是有利的，但是从泛化的观点看是不利的。我们对于初始点如何影响泛化的理解是相当原始的，几乎没有提供如何选择初始点的任何指导。\n\n### 1.1，进行网络参数初始化的原因\n\n深度学习模型（神经网络模型）的训练算法通常是**迭代**的，因此模型训练者需要指定开始迭代的初始点，即择网络参数初始化策略。\n\n### 1.2，网络参数初始化为什么重要\n\n训练深度学习模型是一个足够困难的问题，以至于大多数算法都很大程度受到网络初始化策略的影响。\n\n**模型迭代的初始点能够决定算法是否收敛**，有些初始点十分不稳定，使得该算法会遭遇数值困难，并可能完全失败。当学习收敛时，初始点可以决定学习收敛得多快，以及是否收敛到一个代价高或低的点。另外，即使是具有同一个损失代价的迭代点也会有差别极大的泛化误差，而迭代初始点也可以影响泛化（误差）。\n\n## 二，权重初始化方式分类\n\n在实际应用中，模型权重参数服从**高斯分布**（`Gaussian distribution`）或**均匀分布**（`uniform distribution`）都是较为**有效**的初始化方式。值得注意的是，这两种分布选择的区别，目前还没有被详尽的研究清楚，能够确定只有，初始分布的大小确实对优化过程的结果和网络泛化能力都有很大的影响。\n> 权重初始化随机值的初始化策略的分布都对模型性能有影响，但是影响的原理（神经网络优化原理）又没有被彻底研究清楚，所谓模型训练，还真不愧是炼丹，部分时候还真得靠炼丹人的经验。\n\n另一种神经网络参数初始化策略总结如下：\n\n1. **加载预训练模型参数初始化**：直接加载在大规模数据集上训练得到模型参数，一定程度上提升模型的泛化能力。\n2. **随机初始化**：注意不能将参数值全部初始化为 `0`，因为如果神经网络第一遍前向传播所有隐层神经网络激活值相同，反向传播权重更新也相同，导致隐藏层的各个神经元没有区分性，导致“对称权重”现象。较好的方式是对每个参数进行**随机初始化**。\n3. **固定参数值初始化**：比如对于偏置（bias）通常用 `0` 初始化，LSTM 遗忘门偏置通常为 1或 2，使时序上的梯度变大，对于 ReLU 神经元，偏置设为 0.01，使得训练初期更容易激活。\n\n虽然不同文章对参数初始化方法的分类有着不同的总结，因此，本文直接给出常用且有效的初始化方法名称，并以 `Pytorch` 框架为例，给出相应 `API`。\n\n### 2.1，全零初始化\n\n虽然参数(权值)在理想情况下应基本保持正负各半的状态(**期望为 0**)，但是，这不意味着可以将所有参数都初始化为 `0`（$W=0$）！零值初始化的权重矩阵值打印输出示例如下所示。\n\n```python\nW1= [[-0.82452497 -0.82452497 -0.82452497]]\nB1= [[-0.01143752 -0.01143752 -0.01143752]]\nW2= [[-0.68583865]\n     [-0.68583865]\n     [-0.68583865]]\nB2= [[0.68359678]] # 单个输出的的双层神经网络\n```\n\n可以看到 `W1、B1、W2` 内部 `3` 个单元的值都一样，这是因为初始值都是 0，所以梯度均匀回传，导致所有神经元的权重 `W` 的值都同步更新，没有任何差别，这样无论训练多少轮，结果也不会正确。\n\n### 2.2，标准初始化\n\n1，高斯分布初始化：使用高斯分布对每个参数随机初始化，即将权重 $W$ 按如下公式进行初始化:\n\n$$\nW \\sim N[0, \\sigma^2]\n$$\n其中 $N$ 表示**高斯分布**（Gaussian Distribution，也叫做正态分布，Normal Distribution），上式是位置参数 $\\mu = 0$（**期望值**），尺度参数 $\\sigma^2$（**方差**） 的高斯分布（也叫标准高斯分布）。有的地方也称为 **`Normal` 初始化**。\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\n# 一般默认采用标准高斯分布初始化方法，即均值为 0，方差为 1，\ntorch.nn.init.normal_(tensor, mean=0, std=1)\n```\n\n2，与高斯分布初始化方式类似的是**均匀分布初始化**，其参数范围区是 $[-r, r]$。\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\ntorch.nn.init.uniform_(tensor, a=0, b=1)\n```\n\n高斯分布和均匀分布都是固定方差参数的初始化方法，它们的关键是：如何设置方差！\n- 如果太小，会导致神经元输出过小，经过多层则梯度信号消失了。\n- 如果太大，`sigmoid` 激活梯度接近 `0`，导致梯度消失。一般需要配合 `BN` 层一起使用。\n\n当目标问题较为简单、网络深度不大时，一般用标准初始化就可以了。但是当使用深度神经网络时，标准初始化在 Sigmoid 激活函数上的表现会遇到如下图 1 所示的问题。\n\n![图1-标准初始化在Sigmoid激活函数上的表现](../images/parameter_init/init_normal_sigmoid.png)\n\n上图是一个 `6`层的深度网络，使用 **全连接层 + Sigmoid 激活函数**的配置，图中表示的是**各层激活函数的直方图**。可以看到各层的激活值严重向两侧 $[0,1]$ 靠近，从 `Sigmoid` 的函数曲线可以知道这些值的导数趋近于 `0`（激活函数值趋近于零，导数也趋近于零），**反向传播时的梯度逐步消失**。处于中间地段的值比较少，对参数学习非常不利。\n> 传统的**固定方差**的高斯分布初始化方法，在网络变深的时候会使得模型很难收敛。\n\n### 2.3，Xavier 初始化\n\n基于上述观察（标准初始化在 `Sigmoid` 激活函数上的表现），Xavier Glorot 等人于 `2010` 年研究出了下面的`Xavier` 初始化方法。\n\n`Xavier` 初始化方法比直接用高斯分布进行初始化 $W$ 的优势在于：一般的神经网络在前向传播时神经元输出值的方差会不断增大，而使用 `Xavier` 等方法理论上可以保证**每层神经元输入输出数据分布方差一致**（和 `BN` 层效果类似，具体原理参考[Xavier 论文](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)）。\n\n条件：正向传播时，激活值的方差保持不变；反向传播时，关于状态值的梯度的方差保持不变。\n\n使用 `Sigmoid` 激活函数时，**Xavier 高斯分布**初始化的公式如下所示:\n\n$$\nW \\sim N\n\\begin{pmatrix}\n0, \\sqrt{\\frac{2}{n_{in} + n_{out}}} \n\\end{pmatrix}\n$$\n\n`Pytorch` 框架中对应的 `API` 如下。\n```python\ntorch.nn.init.xavier_normal_(tensor, gain=1)\n```\n\n另外，还有 Xavier 均匀分布的公式如下所示（个人感觉使用频率不多）。\n\n$$\nW \\sim U \n\\begin{pmatrix}\n -\\sqrt{\\frac{6}{n_{in} + n_{out}}}, \\sqrt{\\frac{6}{n_{in} + n_{out}}} \n\\end{pmatrix}\n$$\n\n下图2展示了 Xavier 初始化在 Sigmoid 激活函数上的表现，其和图1都基于同一个深度为 `6` 层的网络。可以看到，后面几层的**激活函数输出值**的分布**仍然**基本符合正态分布，这有利于神经网络的学习。\n\n![图2-Xavier初始化在Sigmoid激活函数上的表现](../images/parameter_init/init_xavier_sigmoid.png)\n\n### 2.4，He 初始化\n\n随着深度学习的发展，人们觉得 `Sigmoid` 激活在反向传播算法中效果有限且会导致梯度消失问题，于是又提出了 `ReLU` 激活函数。\n\n但 Xavier 初始化在 ReLU 激活函数上的表现并不好。随着网络层的加深，使用 `ReLU` 时激活值逐步向 `0` 偏向，这同样会导致梯度消失问题。\n\n下图3展现了 Xavier 初始化在 ReLU 激活函数上的表现。\n\n![Xavier初始化在ReLU激活函数上的表现](../images/parameter_init/init_xavier_relu.png)\n\n于是 He Kaiming 等人于 2015 年提出了 He 初始化法（也叫做MSRA初始化法）。\n\nHe 初始化法最主要是想解决使用 `ReLU` 激活函数后，方差会发生变化的问题。\n\n只考虑输入个数时，He 初始化是一个均值为 0，方差为 $2/n$ 的高斯分布，适合于 ReLU 激活函数:\n\n$$\nW \\sim N \n\\begin{pmatrix} \n0, \\sqrt{\\frac{2}{n}} \n\\end{pmatrix}\n$$\n\n其中 $n$ 为网络层输入神经元数量个数。\n\n`Pytorch` 框架中对应的 `API` 如下。\n\n```python\ntorch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')\n```\n\n下图4 为 He 初始化在 ReLU 激活函数上的表现，所用网络和前面一样。\n\n![图4-He初始化在ReLU激活函数上的表现](../images/parameter_init/init_msra_relu.png)\n\n### 2.5，总结\n\n- Xavier 初始化和 He 初始化目的都是想**可以根据神经元连接数量自适应调整初始化分布的方差，也称为“方差缩放”**。\n- 网络参数初始化方法的选择对保持数值稳定性至关重要，其与非线性激活函数的选择得结合一起考虑。\n- 网络参数初始化的选择可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练神经网络模型时遇到梯度爆炸或梯度消失。\n\n除了以上初始化，比较常用的还有“随机初始化 With BN”和“预训练模型初始化”，各种初始化方法的特点总结如下:\n\n![参数初始化方法总结](../images/parameter_init/parameter_init_summary.png)\n## 参考资料\n\n- 《深度学习-8.4 参数初始化策略》\n- 《解析卷积神经网络-章 7 网络参数初始化》\n- [AIEDU-15.1 权重矩阵初始化](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.1-%E6%9D%83%E9%87%8D%E7%9F%A9%E9%98%B5%E5%88%9D%E5%A7%8B%E5%8C%96.html)\n- [神经网络之权重初始化](https://www.cnblogs.com/makefile/p/init-weight.html)\n- [神经网络参数初始化小结](https://zhuanlan.zhihu.com/p/133835463)"
  },
  {
    "path": "2-deep_learning_basic/深度学习基础-损失函数详解.md",
    "content": "- [一，损失函数概述](#一损失函数概述)\n- [二，交叉熵函数-分类损失](#二交叉熵函数-分类损失)\n  - [2.1，交叉熵（Cross-Entropy）的由来](#21交叉熵cross-entropy的由来)\n    - [2.1.1，熵、相对熵以及交叉熵总结](#211熵相对熵以及交叉熵总结)\n  - [2.2，二分类问题的交叉熵](#22二分类问题的交叉熵)\n  - [2.3，多分类问题的交叉熵](#23多分类问题的交叉熵)\n  - [2.4，PyTorch 中的 Cross Entropy](#24pytorch-中的-cross-entropy)\n    - [2.4.1，Softmax 多分类函数](#241softmax-多分类函数)\n  - [2.5，为什么不能使用均方差做为分类问题的损失函数？](#25为什么不能使用均方差做为分类问题的损失函数)\n- [三，回归损失](#三回归损失)\n  - [3.1，MAE 损失](#31mae-损失)\n  - [3.2，MSE 损失](#32mse-损失)\n  - [3.3，`Huber` 损失](#33huber-损失)\n  - [3.4，代码实现](#34代码实现)\n- [参考资料](#参考资料)\n\n## 一，损失函数概述\n\n大多数深度学习算法都会涉及某种形式的优化，**所谓优化指的是改变 $x$ 以最小化或最大化某个函数 $f(x)$ 的任务**，我们通常以最小化 $f(x)$ 指代大多数最优化问题。\n\n在机器学习中，损失函数是代价函数的一部分，而代价函数是目标函数的一种类型。\n- **损失函数**（`loss function`）: 用于定义单个训练样本预测值与真实值之间的误差\n- **代价函数**（`cost function`）: 用于定义单个批次/整个训练集样本预测值与真实值之间的累计误差。\n- **目标函数**（`objective function`）: 泛指任意可以被优化的函数。\n\n**损失函数定义**：损失函数是用来量化模型预测和真实标签之间差异的一个非负实数函数，其和优化算法紧密联系。深度学习算法优化的第一步便是确定损失函数形式。\n\n损失函数大致可分为两种：回归损失（针对连续型变量）和分类损失（针对离散型变量），其在深度学习实验流程中的位置如下图所示。\n\n![深度学习的实验流程](../images/loss/define_loss.png)\n\n> 图片来源李宏毅 2022 机器学习暑期课程-[Machine Learning Pytorch Tutorial](https://speech.ee.ntu.edu.tw/~hylee/ml/ml2022-course-data/Pytorch%20Tutorial%201.pdf)。\n\n常用的减少损失函数的优化算法是“梯度下降法”（Gradient Descent）。\n\n## 二，交叉熵函数-分类损失\n\n交叉熵损失(`Cross-Entropy Loss`) 又称为对数似然损失(Log-likelihood Loss)、对数损失，二分类时还可称之为逻辑斯谛回归损失(Logistic Loss)。\n\n### 2.1，交叉熵（Cross-Entropy）的由来\n> 交叉熵损失的由来参考文档 [AI-EDU: 交叉熵损失函数](https://microsoft.github.io/ai-edu/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/)。\n\n**1，信息量**\n\n信息论中，信息量的表示方式：\n> 《深度学习》（花书）中称为自信息(self-information) 。\n> 在本文中，我们总是用 $\\text{log}$ 来表示自然对数，**其底数**为 $e$。\n\n$$\nI(x_j) = -\\log (p(x_j))\n$$\n\n- $x_j$：表示一个事件\n- $p(x_j)$：表示事件 $x_j$ 发生的概率\n- $I(x_j)$：信息量，$x_j$ 越不可能发生时，它一旦发生后的信息量就越大\n\n**2，熵**\n\n信息量只处理单个的输出。我们可以用熵（也称香农熵 `Shannon entropy`）来对整个概率分布中的不确定性总量进行量化:\n\n$$\nH(p) = - \\sum_j^n p(x_j) \\log (p(x_j))\n$$\n\n则上面的问题的熵是：\n$$\n\\begin{aligned} H(p)&=-[p(x_1) \\ln p(x_1) + p(x_2) \\ln p(x_2) + p(x_3) \\ln p(x_3)] \\\\\\ &=0.7 \\times 0.36 + 0.2 \\times 1.61 + 0.1 \\times 2.30 \\\\\\ &=0.804 \\end{aligned}\n$$\n**3，相对熵(KL散度)**\n\n相对熵又称 `KL` 散度，如果对于同一个随机变量 $x$ 有两个单独的概率分布 $P(x)$ 和 $Q(x)$，则可以使用 KL 散度（Kullback-Leibler (KL) divergence）来**衡量这两个分布的差异**，这个相当于信息论范畴的均方差。\n\nKL散度的计算公式：\n\n$$\nD_{KL}(p||q)=\\sum_{j=1}^m p(x_j) \\log {p(x_j) \\over q(x_j)}\n$$\n\n$m$ 为事件的所有可能性（分类任务中对应类别数目）。**$D$ 的值越小，表示 $q$ 分布和 $p$ 分布越接近**。\n\n**4，交叉熵**\n\n把上述交叉熵公式变形：\n\n$$\n\\begin{aligned} D_{KL}(p||q)&=\\sum_{j=1}^m p(x_j) \\log {p(x_j)} - \\sum_{j=1}^m p(x_j) \\log q(x_j) \\\\\\ &=- H(p(x)) + H(p,q) \\end{aligned}\n$$\n\n等式的前一部分恰巧就是 $p$ 的熵，等式的后一部分，就是**交叉熵**（机器学习中 $p$ 表示真实分布（目标分布），$q$ 表示预测分布）:\n\n$$\nH(p,q) =- \\sum_{j=1}^m p(x_j) \\log q(x_j)\n$$\n\n在机器学习中，我们需要评估**标签值 $y$ 和预测值 $a$** 之间的差距熵（即**两个概率分布之间的相似性**），使用 KL 散度 $D_{KL}(y||a)$ 即可，但因为样本标签值的分布通常是固定的，即 $H(a)$ 不变。因此，为了计算方便，在优化过程中，只需要关注交叉熵就可以了。所以，**在机器学习中一般直接用交叉熵做损失函数来评估模型**。\n\n$$\nloss = \\sum_{j = 1}^{m}y_{j}\\text{log}(a_{j})\n$$\n\n上式是单个样本的情况，$m$ **并不是样本个数，而是分类个数**。所以，对于**批量样本的交叉熵损失**计算公式（很重要!）是：\n\n$$\nJ = -\\frac{1}{n}\\sum_{i=1}^n \\sum_{j=1}^{m} y_{ij} \\log a_{ij}\n$$\n\n其中，$n$ 是样本数，$m$ 是分类数。\n> 公式参考文章-[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%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/03.2-%E4%BA%A4%E5%8F%89%E7%86%B5%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0.html)，但是将样本数改为 $n$，类别数改为 $m$。\n\n有一类特殊问题，就是事件只有两种情况发生的可能，比如“是狗”和“不是狗”，称为 $0/1$ 分类或**二分类**。对于这类问题，由于 $m=2，y_1=1-y_2，a_1=1-a_2$，所以**二分类问题的单个样本的交叉熵**可以简化为：\n\n$$\nloss =-[y \\log a + (1-y) \\log (1-a)]\n$$\n\n**二分类对于批量样本的交叉熵**计算公式是：\n\n$$\nJ= -\\frac{1}{n} \\sum_{i=1}^n [y_i \\log a_i + (1-y_i) \\log (1-a_i)]\n$$\n> 为什么交叉熵的代价函数是求均值而不是求和?\n>  Cross entropy loss is defined as the “expectation” of the probability distribution of a random variable 𝑋, and that’s why we use mean instead of sum. 参见[这里](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html#cross-entropy)。\n\n#### 2.1.1，熵、相对熵以及交叉熵总结\n\n> 交叉熵 $H(p, q)$ 也记作 $CE(p, q)$、$H(P, Q)$，其另一种表达公式（公式表达形式虽然不一样，但是意义相同）:\n> $$H(P, Q)  = -\\mathbb{E}_{\\textrm{x}\\sim p}log(q(x))$$\n\n交叉熵函数常用于逻辑回归(`logistic regression`)，也就是分类(`classification`)。\n\n根据信息论中熵的性质，将熵、相对熵（KL 散度）以及交叉熵的公式放到一起总结如下:\n\n$$\\begin{aligned}\nH(p) &= -\\sum_{j}p(x_j) \\log p(x_j) \\\\\nD_{KL}(p \\parallel q) &= \\sum_{j}p(x_j)\\log \\frac{p(x_j)}{q(x_j)} = \\sum_j (p(x_j)\\log p(x_j) - p(x_j) \\log q(x_j)) \\\\\nH(p,q) &=  -\\sum_j p(x_j)\\log q(x_j) \\\\\n\\end{aligned} $$\n\n### 2.2，二分类问题的交叉熵\n\n把二分类的交叉熵公式 4 分解开两种情况：\n- 当 $y=1$ 时，即标签值是 $1$ ，是个正例，加号后面的项为: $loss = -\\log(a)$\n- 当 $y=0$ 时，即标签值是 $0$，是个反例，加号前面的项为 $0$: $loss = -\\log (1-a)$\n\n横坐标是预测输出，纵坐标是损失函数值。$y=1$ 意味着当前样本标签值是1，当预测输出越接近1时，损失函数值越小，训练结果越准确。当预测输出越接近0时，损失函数值越大，训练结果越糟糕。此时，损失函数值如下图所示。\n\n![二分类交叉熵损失函数图](../images/loss/binary_cross_entropy_loss_function_diagram.png)\n\n### 2.3，多分类问题的交叉熵\n\n当标签值不是非0即1的情况时，就是多分类了。\n\n假设希望根据图片动物的轮廓、颜色等特征，来预测动物的类别，有三种可预测类别：猫、狗、猪。假设我们训练了两个分类模型，其预测结果如下:\n\n**模型1**:\n\n|预测值|标签值|是否正确|\n|-----|-----|-------|\n|0.3 0.3 0.4|0 0 1（猪）|正确|\n|0.3 0.4 0.4|0 1 0（狗）|正确|\n|0.1 0.2 0.7|1 0 0（猫）|错误|\n\n每行表示不同样本的预测情况，公共 3 个样本。可以看出，模型 1 对于样本 1 和样本 2 以非常微弱的优势判断正确，对于样本 3 的判断则彻底错误。\n\n**模型2**:\n\n|预测值|标签值|是否正确|\n|-----|-----|-------|\n|0.1 0.2 0.7|0 0 1（猪）|正确|\n|0.1 0.7 0.2|0 1 0（狗）|正确|\n|0.3 0.4 0.4|1 0 0（猫）|错误|\n\n可以看出，模型 2 对于样本 1 和样本 2 判断非常准确（预测概率值更趋近于 1），对于样本 3 虽然判断错误，但是相对来说没有错得太离谱（预测概率值远小于 1）。\n\n结合多分类的交叉熵损失函数公式可得，模型 1 的交叉熵为:\n\n$$\\begin{aligned} \n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.3) + 0\\times log(0.3) + 1\\times log(0.4) = 0.91 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.3) + 1\\times log(0.4) + 0\\times log(0.4) = 0.91 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(1\\times log(0.1) + 0\\times log(0.2) + 0\\times log(0.7) = 2.30\n\\end{aligned}$$\n\n对所有样本的 `loss` 求平均:\n\n$$\nL = \\frac{0.91 + 0.91 + 2.3}{3} = 1.37\n$$\n\n模型 2 的交叉熵为:\n\n$$\\begin{aligned} \n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.1) + 0\\times log(0.2) + 1\\times log(0.7) = 0.35 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(0\\times log(0.1) + 1\\times log(0.7) + 0\\times log(0.2) = 0.35 \\\\\n\\text{sample}\\ 1\\ \\text{loss} = -(1\\times log(0.3) + 0\\times log(0.4) + 0\\times log(0.4) = 1.20\n\\end{aligned} $$\n\n对所有样本的 `loss` 求平均:\n\n$$\nL = \\frac{0.35 + 0.35 + 1.2}{3} = 0.63\n$$\n\n可以看到，0.63 比 1.37 的损失值小很多，这说明预测值越接近真实标签值，即交叉熵损失函数可以较好的捕捉到模型 1 和模型 2 预测效果的差异。**交叉熵损失函数值越小，反向传播的力度越小**。\n> 多分类问题计算交叉熵的实例来源于知乎文章-[损失函数｜交叉熵损失函数](https://zhuanlan.zhihu.com/p/35709485)。\n\n### 2.4，PyTorch 中的 Cross Entropy\n\nPyTorch 中常用的交叉熵损失函数为 `torch.nn.CrossEntropyLoss`\n\n```python\nclass torch.nn.CrossEntropyLoss(weight=None, size_average=None,\n                                ignore_index=-100, reduce=None, \n                                reduction='elementwise_mean')\n```\n\n**1，函数功能**:\n\n将输入经过 `softmax` 激活函数之后，再计算其与 `target` 的交叉熵损失。即该方法将 `nn.LogSoftmax()` 和 `nn.NLLLoss()`进行了结合。严格意义上的交叉熵损失函数应该是 `nn.NLLLoss()`。\n\n**2，参数解释**:\n\n- `weight`(Tensor)- 为每个类别的 loss 设置权值，常用于类别不均衡问题。weight 必须是 float 类型的 tensor，其长度要于类别 `C` 一致，即每一个类别都要设置有 weight。\n- `size_average`(bool)- 当 reduce=True 时有效。为 True 时，返回的 loss 为平均值;为 False 时，返回的各样本的 loss 之和。\n- `reduce`(bool)- 返回值是否为标量，默认为 True。\n- `ignore_index`(int)- 忽略某一类别，不计算其 `loss`，其 loss 会为 0，并且，在采用 size_average 时，不会计算那一类的 loss，除的时候的分母也不会统计那一类的样本。\n\n#### 2.4.1，Softmax 多分类函数\n> 注意: Softmax 用作模型最后一层的函数通常和交叉熵作损失函数配套搭配使用，应用于多分类任务。\n\n对于二分类问题，我们使用 `Logistic` 函数计算样本的概率值，从而把样本分成了正负两类。对于多分类问题，则使用 `Softmax` 作为模型最后一层的激活函数来将**多分类的输出值转换为范围在 [0, 1] 和为 1 的概率分布**。\n\nSoftmax 从字面上来说，可以分成 soft 和 max 两个部分。max 故名思议就是最大值的意思。Softmax 的核心在于 soft，而 soft 有软的含义，与之相对的是 hard 硬，即 herdmax。下面分布演示将模型输出值**取 max 值**和**引入 Softmax** 的对比情况。\n\n**取max值（hardmax）**\n\n假设模型输出结果 $z$ 值是 $[3,1,-3]$，如果取 max 操作会变成 $[1, 0, 0]$，这符合我们的分类需要，即三者相加为1，并且认为该样本属于第一类。但是有两个不足：\n\n1. 分类结果是 $[1,0,0]$，只保留非 0 即 1 的信息，即非黑即白，没有各元素之间相差多少的信息，可以理解是“Hard Max”；\n2. max 操作本身不可导，无法用在反向传播中。\n\n**引入Softmax**\n\n`Softmax` 加了个\"soft\"来模拟 max 的行为，但同时又保留了相对大小的信息。\n\n$$\na_j = \\text{Softmax}(z_j) = \\frac{e^{z_j}}{\\sum\\limits_{i=1}^m e^{z_i}}=\\frac{e^{z_j}}{e^{z_1}+e^{z_2}+\\dots+e^{z_m}}\n$$\n\n上式中:\n\n- $z_j$ 是对第 $j$ 项的分类原始值，即矩阵运算的结果\n- $z_i$ 是参与分类计算的每个类别的原始值\n- $m$ 是总分类数\n- $a_j$ 是对第 $j$ 项的计算结果\n\n和 hardmax 相比，Softmax 的含义就在于不再唯一的确定某一个最大值，而是为每个输出分类的结果都赋予一个概率值（置信度），表示属于每个类别的可能性。\n\n下图可以形象地说明 Softmax 的计算过程。\n\n![Softmax工作过程](../images/loss/softmax_process.png)\n\n当输入的数据 $[z_1,z_2,z_3]$ 是 $[3, 1, -3]$ 时，按照图示过程进行计算，可以得出输出的概率分布是 $[0.879,0.119,0.002]$。对比 max 运算和 Softmax 的不同，如下表所示。\n\n|输入原始值|MAX计算|Softmax计算|\n|--------|-------|----------|\n|$[3, 1, -3]$|$[1, 0, 0]$|$[0.879, 0.119, 0.002]$|\n\n可以看出 Softmax 运算结果两个特点：\n\n1. 三个类别的概率相加为 1\n2. 每个类别的概率都大于 0\n\n下面我再给出 hardmax 和 softmax 计算的代码实现。\n\n```python\n# example of the argmax of a list of numbers\nfrom numpy import argmax\nfrom numpy import exp\n\n# define data\ndata = [3, 1, -3]\n\ndef hardmax(data):\n    \"\"\"# calculate the argmax of the list\"\"\"\n    result = argmax(data) \n    return result\n\ndef softmax(vector):\n    \"\"\"# calculate the softmax of a vector\"\"\"\n    e = exp(vector)\n    return e / e.sum()\n\nhardmax_result = hardmax(data)\n# 运行该示例返回列表索引值“0”，该值指向包含列表“3”中最大值的数组索引 [1]。\nprint(hardmax(data)) # 0\n\n# convert list of numbers to a list of probabilities\nsoftmax_result = softmax(data) \nprint(softmax_result) # report the probabilities\nprint(sum(softmax_result)) # report the sum of the probabilitie\n```\n\n运行以上代码后，输出结果如下:\n> 0\n[0.87887824 0.11894324 0.00217852]\n1.0\n\n很明显程序的输出结果和我们手动计算的结果是一样的。\n\nPytorch 中的 Softmax 函数定义如下:\n\n```python\ndef softmax(x):\n    return torch.exp(x)/torch.sum(torch.exp(x), dim=1).view(-1,1)\n```\n\n`dim=1` 用于 `torch.sum()` 对所有列的每一行求和，`.view(-1,1)` 用于防止广播。\n\n### 2.5，为什么不能使用均方差做为分类问题的损失函数？\n\n回归问题通常用均方差损失函数，可以保证损失函数是个凸函数，即可以得到最优解。而分类问题如果用均方差的话，损失函数的表现不是凸函数，就很难得到最优解。而交叉熵函数可以保证区间内单调。\n\n分类问题的最后一层网络，需要分类函数，`Sigmoid` 或者 `Softmax`，如果再接均方差函数的话，其求导结果复杂，运算量比较大。用交叉熵函数的话，可以得到比较简单的计算结果，一个简单的减法就可以得到反向误差。\n\n## 三，回归损失\n\n与分类问题不同，回归问题解决的是**对具体数值的预测**。解决回归问题的神经网络一般只有只有一个输出节点，这个节点的输出值就是预测值。\n\n回归问题的一个基本概念是**残差**或称为**预测误差**，用于衡量模型预测值与真实标记的靠近程度。假设回归问题中对应于第 $i$ 个输入特征 $x_i$ 的**标签**为 $y^i = (y_1,y_2,...,y_M)^{\\top}$，$M$ 为标记向量总维度，则 $l_{t}^{i}$ 即表示样本 $i$ 上神经网络的回归预测值 ($y^i$) 与其样本标签值在第 $t$ 维的预测误差(亦称残差):\n\n$$\nl_{t}^{i} = y_{t}^{i} - \\hat{y}_{t}^{i}\n$$\n\n常用的两种损失函数为 $\\text{MAE}$（也叫 `L1` 损失） 和 $\\text{MSE}$ 损失函数（也叫 `L2` 损失）。\n\n\n### 3.1，MAE 损失\n\n平均绝对误差（Mean Absolute Error，`MAE`）是用于回归模型的最简单但最强大的损失函数之一。\n\n因为存在离群值（与其余数据差异很大的值），所以回归问题可能具有本质上不是严格高斯分布的变量。 在这种情况下，平均绝对误差将是一个理想的选择，因为它没有考虑异常值的方向（不切实际的高正值或负值）。\n\n顾名思义，MAE 是**目标值和预测值之差的绝对值之和**。$n$ 是数据集中数据点的总数，其公式如下:\n$$\n\\text{MAE loss} = \\frac{1}{n}\\sum_{i=1}^{N}\\sum_{t=1}^{M} |y_{t}^{i} - \\hat{y}_{t}^{i}|\n$$\n\n### 3.2，MSE 损失\n\n均方误差（Mean Square Error, `MSE`）几乎是每个数据科学家在回归损失函数方面的偏好，这是因为**大多数变量都可以建模为高斯分布**。\n\n均方误差计算方法是求**预测值与真实值之间距离的平方和**。预测值和真实值越接近，两者的均方差就越小。公式如下:\n\n$$\n\\text{MSE loss} = \\frac{1}{n}\\sum_{i=1}^{N}\\sum_{t=1}^{M} (y_{t}^{i} - \\hat{y}_{t}^{i})^2\n$$\n\n### 3.3，`Huber` 损失\n\nMAE 和 MSE 损失之间的比较产生以下结果：\n\n1. **MAE 损失比 MSE 损失更稳健**。仔细查看公式，可以观察到如果预测值和实际值之间的差异很大，与 MAE 相比，MSE 损失会放大效果。 由于 MSE 会屈服于异常值，因此 MAE 损失函数是更稳健的损失函数。\n\n2. **MAE 损失不如 MSE 损失稳定**。由于 MAE 损失处理的是距离差异，因此一个小的水平变化都可能导致回归线波动很大。在多次迭代中发生的影响将导致迭代之间的斜率发生显著变化。总结就是，MSE 可以确保回归线轻微移动以对数据点进行小幅调整。\n3. **MAE 损失更新的梯度始终相同**。即使对于很小的损失值，梯度也很大。这样不利于模型的学习。为了解决这个缺陷，我们可以使用变化的学习率，在损失接近最小值时降低学习率。\n4. **MSE 损失的梯度随损失增大而增大，而损失趋于0时则会减小**。其使用固定的学习率也可以有效收敛。\n\nHuber Loss 结合了 MAE 的稳健性和 MSE 的稳定性，本质上是 MAE 和 MSE 损失中最好的。**对于大误差，它是线性的，对于小误差，它本质上是二次的**。\n\nHuber Loss 的特征在于参数 $\\delta$。当 $\\vert y − \\hat{y} \\vert$ 小于一个事先指定的值 $\\delta $ 时，变为平方损失，大于 $\\delta $ 时，则变成类似于绝对值损失，因此其是比较robust 的损失函数。其定义如下:\n\n$$\\text{Huber loss} = \\left \\lbrace \\begin{matrix}\n\\frac12[y_{t}^{i} - \\hat{y}_{t}^{i}]^2 & |y_{t}^{i} - \\hat{y}_{t}^{i}| \\leq \\delta \\\\ \n\\delta|y_{t}^{i} - \\hat{y}_{t}^{i}| - \\frac12\\delta^2 & |y_{t}^{i} - \\hat{y}_{t}^{i})| > \\delta\n\\end{matrix}\\right.$$\n\n三种回归损失函数的曲线图比较如下：\n\n![loss_for_regression](../images/loss/loss_for_regression.png)\n> 代码来源 [Loss Function Plot.ipynb](https://nbviewer.org/github/massquantity/Loss-Functions/blob/master/Loss%20Function%20Plot.ipynb)。\n\n三种回归损失函数的其他形式定义如下:\n\n![three_regression_loss](../images/activation_function/three_regression_loss.png)\n\n### 3.4，代码实现\n\n下面是三种回归损失函数的 python 代码实现，以及对应的 `sklearn` 库的内置函数。\n\n```python\n# true: Array of true target variable\n# pred: Array of predictions\ndef mse(true, pred):\n    return np.sum((true - pred)**2)\n\ndef mae(true, pred):\n    return np.sum(np.abs(true - pred))\n\ndef huber(true, pred, delta):\n    loss = np.where(np.abs(true-pred) < delta , 0.5*((true-pred)**2),delta*np.abs(true - pred) - 0.5*(delta**2))\n\n    return np.sum(loss)\n\n# also available in sklearn\nfrom sklearn.metrics import mean_squared_error\nfrom sklearn.metrics import mean_absolute_error\n```\n\n## 参考资料\n\n1. [《动手学深度学习-22.11. Information Theory》](https://d2l.ai/chapter_appendix-mathematics-for-deep-learning/information-theory.html#cross-entropy)\n2. [损失函数｜交叉熵损失函数](https://zhuanlan.zhihu.com/p/35709485)\n3. [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%AC1%E6%AD%A5%20-%20%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86/03.2-%E4%BA%A4%E5%8F%89%E7%86%B5%E6%8D%9F%E5%A4%B1%E5%87%BD%E6%95%B0.html)\n4. [常见回归和分类损失函数比较](https://www.cnblogs.com/massquantity/p/8964029.html)\n5. 《PyTorch_tutorial_0.0.5_余霆嵩》\n6. https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html\n7. [一文详解Softmax函数](https://zhuanlan.zhihu.com/p/105722023)\n8. [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/en-us/Step3%20-%20LinearClassification/07.1-%E5%A4%9A%E5%88%86%E7%B1%BB%E5%87%BD%E6%95%B0.html#711-softmax)\n"
  },
  {
    "path": "2-deep_learning_basic/深度学习基础总结.md",
    "content": "---\nlayout: post\ntitle: 深度学习基础总结\ndate: 2021-10-10 12:00:00\nsummary: 深度学习基础知识总结。\ncategories: DeepLearning\n---\n\n\n- [一，滤波器与卷积核](#一滤波器与卷积核)\n- [二，卷积层和池化输出大小计算](#二卷积层和池化输出大小计算)\n  - [2.1，CNN 中术语解释](#21cnn-中术语解释)\n  - [2.2，卷积输出大小计算（简化型）](#22卷积输出大小计算简化型)\n  - [2.3，理解边界效应与填充 padding](#23理解边界效应与填充-padding)\n  - [参考资料](#参考资料)\n- [三，深度学习框架的张量形状格式](#三深度学习框架的张量形状格式)\n- [四，Pytorch 、Keras 的池化层函数理解](#四pytorch-keras-的池化层函数理解)\n  - [4.1，torch.nn.MaxPool2d](#41torchnnmaxpool2d)\n  - [4.2，keras.layers.MaxPooling2D](#42keraslayersmaxpooling2d)\n- [五，Pytorch 和 Keras 的卷积层函数理解](#五pytorch-和-keras-的卷积层函数理解)\n  - [5.1，torch.nn.Conv2d](#51torchnnconv2d)\n  - [5.2，keras.layers.Conv2D](#52keraslayersconv2d)\n  - [5.3，总结](#53总结)\n- [六，softmax 回归](#六softmax-回归)\n- [七，交叉熵损失函数](#七交叉熵损失函数)\n  - [7.1，为什么交叉熵可以用作代价函数](#71为什么交叉熵可以用作代价函数)\n  - [7.2，优化算法理解](#72优化算法理解)\n- [八，感受野理解](#八感受野理解)\n  - [8.1，感受野大小计算](#81感受野大小计算)\n- [九，卷积和池化操作的作用](#九卷积和池化操作的作用)\n  - [参考资料](#参考资料-1)\n- [十，卷积层与全连接层的区别](#十卷积层与全连接层的区别)\n- [十一，CNN 权值共享问题](#十一cnn-权值共享问题)\n- [十二，CNN 结构特点](#十二cnn-结构特点)\n  - [Reference](#reference)\n- [十三，深度特征的层次性](#十三深度特征的层次性)\n- [十四，什么样的数据集不适合深度学习](#十四什么样的数据集不适合深度学习)\n- [十五，什么造成梯度消失问题](#十五什么造成梯度消失问题)\n- [十六，Overfitting 和 Underfitting 问题](#十六overfitting-和-underfitting-问题)\n  - [16.1，过拟合问题怎么解决](#161过拟合问题怎么解决)\n  - [16.2，如何判断深度学习模型是否过拟合](#162如何判断深度学习模型是否过拟合)\n  - [16.3，欠拟合怎么解决](#163欠拟合怎么解决)\n  - [16.4，如何判断模型是否欠拟合](#164如何判断模型是否欠拟合)\n- [十七，L1 和 L2 区别](#十七l1-和-l2-区别)\n- [十八，TensorFlow计算图概念](#十八tensorflow计算图概念)\n- [十九，BN（批归一化）的作用](#十九bn批归一化的作用)\n- [二十，什么是梯度消失和爆炸](#二十什么是梯度消失和爆炸)\n  - [梯度消失和梯度爆炸产生的原因](#梯度消失和梯度爆炸产生的原因)\n  - [如何解决梯度消失和梯度爆炸问题](#如何解决梯度消失和梯度爆炸问题)\n- [二十一，RNN循环神经网络理解](#二十一rnn循环神经网络理解)\n- [二十二，训练过程中模型不收敛，是否说明这个模型无效，导致模型不收敛的原因](#二十二训练过程中模型不收敛是否说明这个模型无效导致模型不收敛的原因)\n- [二十三，VGG 使用 2 个 3\\*3 卷积的优势](#二十三vgg-使用-2-个-33-卷积的优势)\n  - [23.1，1\\*1 卷积的主要作用](#23111-卷积的主要作用)\n- [二十四，Relu比Sigmoid效果好在哪里？](#二十四relu比sigmoid效果好在哪里)\n  - [参考链接](#参考链接)\n- [二十五，神经网络中权值共享的理解](#二十五神经网络中权值共享的理解)\n  - [参考资料](#参考资料-2)\n- [二十六，对 fine-tuning(微调模型的理解)，为什么要修改最后几层神经网络权值？](#二十六对-fine-tuning微调模型的理解为什么要修改最后几层神经网络权值)\n  - [参考资料](#参考资料-3)\n- [二十七，什么是 dropout?](#二十七什么是-dropout)\n  - [27.1，dropout具体工作流程](#271dropout具体工作流程)\n  - [27.2，dropout在神经网络中的应用](#272dropout在神经网络中的应用)\n  - [27.3，如何选择dropout 的概率](#273如何选择dropout-的概率)\n  - [参考资料](#参考资料-4)\n- [二十八，HOG 算法原理描述](#二十八hog-算法原理描述)\n  - [HOG特征原理](#hog特征原理)\n  - [HOG特征检测步骤](#hog特征检测步骤)\n  - [参考资料](#参考资料-5)\n- [二十九，激活函数](#二十九激活函数)\n  - [29.1，激活函数的作用](#291激活函数的作用)\n  - [29.2，常见的激活函数](#292常见的激活函数)\n  - [29.3，激活函数理解及函数梯度图](#293激活函数理解及函数梯度图)\n- [三十，卷积层和池化层有什么区别](#三十卷积层和池化层有什么区别)\n- [三十一，卷积层和池化层参数量计算](#三十一卷积层和池化层参数量计算)\n- [三十二，神经网络为什么用交叉熵损失函数](#三十二神经网络为什么用交叉熵损失函数)\n- [三十三，数据增强方法有哪些](#三十三数据增强方法有哪些)\n  - [33.1，离线数据增强和在线数据增强有什么区别?](#331离线数据增强和在线数据增强有什么区别)\n  - [Reference](#reference-1)\n- [三十四，ROI Pooling替换为ROI Align的效果，及各自原理](#三十四roi-pooling替换为roi-align的效果及各自原理)\n  - [ROI Pooling原理](#roi-pooling原理)\n  - [ROI Align原理](#roi-align原理)\n  - [RoiPooling 和 RoiAlign 总结](#roipooling-和-roialign-总结)\n  - [Reference](#reference-2)\n- [三十五，CNN的反向传播算法推导](#三十五cnn的反向传播算法推导)\n- [三十六，Focal Loss 公式](#三十六focal-loss-公式)\n- [三十七，快速回答](#三十七快速回答)\n  - [37.1，为什么 Faster RCNN、Mask RCNN 需要使用 ROI Pooling、ROI Align?](#371为什么-faster-rcnnmask-rcnn-需要使用-roi-poolingroi-align)\n  - [37.2，softmax公式](#372softmax公式)\n  - [37.3，上采样方法总结](#373上采样方法总结)\n  - [37.4，移动端深度学习框架知道哪些，用过哪些？](#374移动端深度学习框架知道哪些用过哪些)\n  - [37.5，如何提升网络的泛化能力](#375如何提升网络的泛化能力)\n  - [37.6，BN算法，为什么要在后面加伽马和贝塔，不加可以吗？](#376bn算法为什么要在后面加伽马和贝塔不加可以吗)\n  - [37.7，验证集和测试集的作用](#377验证集和测试集的作用)\n- [三十八，交叉验证的理解和作用](#三十八交叉验证的理解和作用)\n- [三十九，介绍一下NMS和IOU的原理](#三十九介绍一下nms和iou的原理)\n- [四十，评估权重通道的重要性](#四十评估权重通道的重要性)\n- [Reference](#reference-3)\n\n## 一，滤波器与卷积核\n\n在只有一个通道的情况下，“卷积核”（`“kernel”`）就相当于滤波器（`“filter”`），这两个概念是可以互换的。一个 `“Kernel”` 更倾向于是 `2D` 的权重矩阵。而 `“filter”` 则是指多个 `kernel` 堆叠的 `3D` 结构。如果是一个 `2D` 的 `filter`，那么两者就是一样的。但是一个`3D` `filter`，在大多数深度学习的卷积中，它是包含 `kernel` 的。**每个卷积核都是独一无二的，主要在于强调输入通道的不同方面**。\n\n## 二，卷积层和池化输出大小计算\n\n> 不管是 `TensorFlow`、`Keras`、`Caffe` 还是 `Pytorch`，其卷积层和池化层的参数默认值可能有所不同，但是最终的卷积输出大小计算公式是一样的。\n\n### 2.1，CNN 中术语解释\n\n卷积层主要参数有下面这么几个：\n\n+ 卷积核 `Kernel` 大小（在 `Tensorflow/keras` 框架中也称为`filter`）；\n+ 填充 `Padding` ；\n+ 滑动步长 `Stride`；\n+ 输出通道数 `Channels`。\n\n### 2.2，卷积输出大小计算（简化型）\n\n1，在 `Pytorch` 框架中，图片（`feature map`）经卷积 `Conv2D` 后**输出大小计算公式**如下：$\\left \\lfloor N = \\frac{W-F+2P}{S}+1 \\right \\rfloor$，其中 $\\lfloor \\rfloor$ 是向下取整符号，用于结果不是整数时进行向下取整（`Pytorch` 的 `Conv2d` 卷积函数的默认参数 `ceil_mode = False`，即默认向下取整, `dilation = 1`）。\n\n+ 输入图片大小 `W×W`（默认输入尺寸为正方形）\n+ `Filter` 大小 `F×F`\n+ 步长 `S`\n+ padding的像素数 `P`\n+ 输出特征图大小 `N×N`\n\n2，特征图经反卷积（也叫转置卷积） `keras-Conv2DTranspose`（`pytorch-ConvTranspose2d`） 后得到的特征图大小计算方式：$out = (in - 1)s -2p + k$，另外还有一个写法：$W = (N - 1)*S - 2P + F$，这可由卷积输出大小计算公式反推得到。$in$ 是输入大小， $k$ 是卷积核大小，$s$ 是滑动步长， `padding` 的像素数 $p$，$out$ 是输出大小。\n\n**反卷积**也称为转置卷积，一般主要用来还原 `feature map` 的尺寸大小，在 `cnn` 可视化，`fcn` 中达到 `pixel classification`，以及 `gan` 中从特征生成图像都需要用到反卷积的操作。反卷积输出结果计算实例。例如，输入：`2x2`， 卷积核大小：`4x4`， 滑动步长：`3`，填充像素为 `0`， 输出：`7x7` ，其计算过程就是， `(2 - 1) * 3 + 4 = 7`。\n\n3，池化层如果设置为不填充像素（对于 `Pytorch`，设置参数`padding = 0`，对于 `Keras/TensorFlow`，设置参数`padding=\"valid\"`），池化得到的特征图大小计算方式: $N=(W-F)/S+1$，这里公式表示的是除法结果向下取整再加 `1`。\n\n总结：对于`Pytorch` 和 `tensorflow` 的卷积和池化函数，卷积函数 `padding` 参数值默认为 `0/\"valid\"`（即不填充），但在实际设计的卷积神经网络中，卷积层一般会填充像素(`same`)，池化层一般不填充像素(`valid`)，**输出 `shape` 计算是向下取整**。注意：当 `stride`为 `1` 的时候，`kernel`为 `3`、`padding`为 `1` 或者 `kernel`为 `5`、`padding`为 `2`，这两种情况可直接得出卷积前后特征图尺寸不变。\n\n> 注意不同的深度学习框架，卷积/池化函数的输出 `shape` 计算会有和上述公式有所不同，我给出的公式是简化版，适合面试题计算，实际框架的计算比这复杂，因为参数更多。\n\n### 2.3，理解边界效应与填充 padding\n\n如果希望输出特征图的空间维度`Keras/TensorFlow` 设置卷积层的过程中可以设置 `padding` 参数值为 `“valid” 或 “same”`。`“valid”` 代表只进行有效的卷积，对边界数据不处理。`“same” 代表 TensorFlow` 会自动对原图像进行补零（表示卷积核可以停留在图像边缘），也就是自动设置 `padding` 值让输出与输入形状相同。\n\n### 参考资料\n\n+ [CNN中的参数解释及计算](https://flat2010.github.io/2018/06/15/%E6%89%8B%E7%AE%97CNN%E4%B8%AD%E7%9A%84%E5%8F%82%E6%95%B0/ \"CNN中的参数解释及计算\")\n+ [CNN基础知识——卷积（Convolution）、填充（Padding）、步长(Stride)](https://zhuanlan.zhihu.com/p/77471866 \"CNN基础知识——卷积（Convolution）、填充（Padding）、步长(Stride)\")\n\n## 三，深度学习框架的张量形状格式\n\n+ 图像张量的形状有两种约定，**通道在前**（`channel-first`）和**通道在后**（`channel-last`）的约定，常用深度学习框架使用的**数据张量形状**总结如下：\n    + `Pytorch/Caffe`: (`N, C, H, W`)；\n    + `TensorFlow/Keras`: (`N, H, W, C`)。\n\n+ 举例理解就是`Pytorch` 的卷积层和池化层的输入 `shape` 格式为 `(N, C, H, W)`，`Keras` 的卷积层和池化层的输入 `shape` 格式为 `(N, H, W, C)`。\n\n值得注意的是 `OpenCV` 读取图像后返回的矩阵 `shape` 的格式是 `（H, W, C）`格式。当 OpenCV 读取的图像为彩色图像时，返回的多通道的 BGR 格式的矩阵（`HWC`），在内存中的存储如下图：\n\n![opencv矩阵存储格式](../images/opencv/opencv_data_format.png)\n\n## 四，Pytorch 、Keras 的池化层函数理解\n\n> 注意：对于 `Pytorch、Keras` 的卷积层和池化层函数，其 **`padding` 参数值都默认为不填充像素**，默认值为 `0`和 `valid`。\n\n### 4.1，torch.nn.MaxPool2d\n\n```python\nclass torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)\n```\n\n二维池化层，默认输入的尺度是`(N, C_in,H,W)`，输出尺度`（N,C_out,H_out,W_out）`。池化层输出尺度的 Width 默认计算公式如下（`ceil_mode= True` 时是向上取整，`Height` 计算同理）:\n\n $$\\left\\lfloor \\frac{W_{in} + 2 * \\text{padding}[0] - \\text{dilation}[0] \\times (\\text{kernel\\_size}[0] - 1) - 1}{\\text{stride[0]}} + 1 \\right\\rfloor$$\n\n**主要参数解释**：\n\n+ `kernel_size`(`int or tuple`)：`max pooling` 的窗口大小。\n+ `stride`(`int or tuple`, `optional)：`max pooling` 的窗口移动的步长。默认值是 `kernel_size`。\n+ `padding`(`int or tuple`, `optional`)：**默认值为 `0`，即不填充像素**。输入的每一条边补充 `0` 的层数。\n+ `dilation`：滑动窗中各元素之间的距离。\n+ `ceil_mode`：默认值为 `False`，即上述公式默认向下取整，如果设为 `True`，计算输出信号大小的时候，公式会使用向上取整。\n\n> `Pytorch` 中池化层默认`ceil mode = false`，而 `Caffe` 只实现了 `ceil mode= true` 的计算方式。\n\n**示例代码：**\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# 大小为3，步幅为2的正方形窗口池\nm = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n# pool of non-square window\ninput = autograd.Variable(torch.randn(20, 16, 50, 32))\noutput = m(input)\nprint(output.shape)  # torch.Size([20, 16, 25, 16])\n```\n\n### 4.2，keras.layers.MaxPooling2D\n\n```python\nkeras.layers.MaxPooling2D(pool_size=(2, 2), strides=None, padding='valid', data_format=None)\n```\n\n对于 `2D` 空间数据的最大池化。默认输入尺寸是 `(batch_size, rows, cols, channels)/(N, H, W, C_in)` 的 `4D` 张量，默认输出尺寸是 `(batch_size, pooled_rows, pooled_cols, channels)` 的 `4D` 张量。\n\n+ `padding = valid`：池化层输出的特征图大小为：$N=(W-F)/S+1$，**这里表示的是向下取整再加 1**。\n+ `padding = same`: 池化层输出的特征图大小为 $N = W/S$，**这里表示向上取整**。\n\n**主要参数解释：**\n\n+ `pool_size`: 整数，或者 `2` 个整数表示的元组， 沿（垂直，水平）方向缩小比例的因数。（`2，2`）会把输入张量的两个维度都缩小一半。 如果只使用一个整数，那么两个维度都会使用同样的窗口长度。\n+ `strides`: 整数，`2` 个整数表示的元组，或者是 `None`。 表示步长值。 如果是 `None`，那么默认值是 `pool_size`。\n+ `padding`: `\"valid\"` 或者 `\"same\"`（区分大小写）。\n+ `data_format`: 字符串，`channels_last` (默认)或 `channels_first` 之一。 表示输入各维度的顺序。 `channels_last` 代表尺寸是 `(batch, height, width, channels)` 的输入张量， 而 `channels_first` 代表尺寸是 `(batch, channels, height, width)` 的输入张量。 默认值根据 `Keras` 配置文件 `~/.keras/keras.json` 中的 `image_data_format` 值来设置。如果还没有设置过，那么默认值就是 `\"channels_last\"`。\n\n## 五，Pytorch 和 Keras 的卷积层函数理解\n\n### 5.1，torch.nn.Conv2d\n\n注意，`2D` 卷积的卷积核权重是一个 `4D` 张量，包含输出通道，输入通道，高，宽。对于 `Pytorch/Caffe` 深度学习框架，其输入输出数据的尺寸都是 （`(N, C, H, W)`），卷积核权重 `shape` 如下：\n- 常规卷积的卷积核权重 `shape`:（`C_out, C_in, kernel_height, kernel_width`）\n- 分组卷积的卷积核权重 `shape`:（`C_out, C_in/g, kernel_height, kernel_width`）\n- `DW` 卷积的卷积核权重`shape`:（`C_in, 1, kernel_height, kernel_width`）\n\n`Pytorch` 框架中对应的 2D 卷积层 api 如下：\n\n```python\nclass torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)\n```\n\n二维卷积层, 输入的尺度是`(N, C_in, H, W)`，输出尺度`（N,C_out,H_out,W_out）`。卷积层输出尺度的 `Weight` 计算公式如下（`Height` 同理）：\n\n$$\\left\\lfloor \\frac{W_{in} + 2 \\times \\text{padding}[0] - \\text{dilation}[0] \\times (\\text{kernel\\_size}[0] - 1) - 1}{\\text{stride}[0]} + 1\\right\\rfloor$$\n\n`kernel_size`, `stride`, `padding`, `dilation` 参数可以是以下两种形式( `Maxpool2D` 也一样)：\n\n+ `a single int`：同样的参数值被应用与 `height` 和 `width` 两个维度。\n+ `a tuple of two ints`：第一个 `int` 值应用于 `height` 维度，第二个 `int` 值应用于 `width` 维度，也就是说卷积输出后的 `height` 和 `width` 值是不同的，要分别计算。\n\n**主要参数解释：**\n\n+ `in_channels`(`int`) – 输入信号的通道。\n+ `out_channels`(`int`) – 卷积产生的通道。\n+ `kerner_size`(`int or tuple`) - 卷积核的尺寸。\n+ `stride`(`int or tuple`, `optional`) - 卷积步长，默认值为 `1` 。\n+ `padding`(`int or tuple`, `optional`) - 输入的每一条边补充 `0` 的层数，默认不填充。\n+ `dilation`(`int or tuple`, `optional`) – 卷积核元素之间的间距，默认取值 `1` 。\n+ `groups`(`int`, `optional`) – 从输入通道到输出通道的阻塞连接数。\n+ `bias`(`bool`, `optional`) - 如果 `bias=True`，添加偏置。\n\n**示例代码：**\n\n```python\n###### Pytorch卷积层输出大小验证\nimport torch\nimport torch.nn as nn\nimport torch.autograd as autograd\n# With square kernels and equal stride\n# output_shape: height = (50-3)/2+1 = 24.5，卷积向下取整，所以 height=24.\nm = nn.Conv2d(16, 33, 3, stride=2)\n# # non-square kernels and unequal stride and with padding\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))  # 输出shape: torch.Size([20, 33, 28, 100])\n# # non-square kernels and unequal stride and with padding and dilation\n# m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2), dilation=(3, 1))  # 输出shape: torch.Size([20, 33, 26, 100])\ninput = autograd.Variable(torch.randn(20, 16, 50, 100))\noutput = m(input)\n\nprint(output.shape)  # 输出shape: torch.Size([20, 16, 24, 49])\n```\n\n### 5.2，keras.layers.Conv2D\n\n```python\nkeras.layers.Conv2D(filters, kernel_size, strides=(1, 1), padding='valid', data_format=None, dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None)\n```\n\n`2D` 卷积层 (例如对图像的空间卷积)。输入输出尺寸格式要求和池化层函数一样。输入尺寸：`(N, H, W, C)`，卷积核尺寸：（`K, K, C_in, C_out`）。\n\n**当使用该层作为模型第一层时，需要提供 `input_shape` 参数**（整数元组，不包含 `batch` 轴），例如，`input_shape=(128, 128, 3)` 表示 `128x128` 的 `RGB` 图像，在 `data_format=\"channels_last\"` 时。\n\n**主要参数解释：**\n\n+ `filters`: 整数，输出空间的维度 （**即卷积中滤波器的输出数量**）。\n+ `kernel_size`: 一个整数，或者 `2` 个整数表示的元组或列表，指明 `2D` 卷积窗口的宽度和高度。 **可以是一个整数，为所有空间维度指定相同的值**。\n+ `strides`: 一个整数，或者 `2` 个整数表示的元组或列表，指明卷积核模板沿宽度和高度方向的移动步长。 可以是一个整数，为所有空间维度指定相同的值。 指定任何 stride 值 != 1 与指定 dilation_rate 值 != 1 两者不兼容，默认取值 1，即代表会不遗漏的滑过输入图片（`Feature Map`）的每一个点。\n+ `padding`: `\"valid\"` 或 `\"same\"` (大小写敏感)，默认`valid`，这里的 `\"same\"` 代表给边界加上 `Padding` 让卷积的输出和输入保持同样（`\"same\"`）的尺寸（即填充像素）。\n+ `data_format`: 字符串， `channels_last (默认)` 或 `channels_first` 之一，表示输入中维度的顺序。 `channels_last` 对应输入尺寸为 `(batch_size, height, width, channels)`， channels_first 对应输入尺寸为 `(batch_size, channels, height, width)`。 它默认为从 `Keras` 配置文件 `~/.keras/keras.json` 中 找到的 `image_data_format` 值。 如果你从未设置它，将使用 `channels_last`。\n+ `dilation_rate`: 一个整数或 `2` 个整数的元组或列表， 指定膨胀卷积（空洞卷积 `dilated convolution`）的膨胀率。 可以是一个整数，为所有空间维度指定相同的值。 当前，指定任何 dilation_rate 值 != 1 与 指定 stride 值 != 1 两者不兼容。\n\n### 5.3，总结\n\n`Pytorch` 的 `Conv2d` 函数不要求提供 输入数据的大小 `(H,W)`，但是要提供输入深度，`Keras` 的 `Conv2d` 函数第一层要求提供 `input_shape` 参数 `(H,W, C)`，其他层不需要。\n\n ## 六，softmax 回归\n\n**分类问题**中，直接使用输出层的输出有两个问题：\n\n+ 神经网络输出层的输出值的范围不确定，我们难以直观上判断这些值的意义\n+ 由于真实标签是离散值，这些离散值与不确定范围的输出值之间的误差难以衡量\n\n`softmax` 回归解决了以上两个问题，它**将输出值变换为值为正且和为 1 的概率分布**，公式如下：\n$$\nsoftmax(y)_{i} = y_{i}^{'} = \\frac{e^{yi}}{\\sum_{j=1}^{n}e^{yj}}\n$$\n\n## 七，交叉熵损失函数\n\n交叉熵刻画了两个概率分布之间的距离，它是分类问题中使用比较广泛的一种损失函数，交叉熵一般会与 `softmax` 回归一起使用，公式如下：\n\n$$L = -\\sum_{c=1}^{M}y_{c}log(p_{c})或者H(p,q)=-\\sum p(x)logq(x)$$\n\n- $p$ ——代表正确答案；\n- $q$ ——代表预测值；\n- $M$ ——类别的数量；\n- $y_{c}$ ——指示变量（ `0` 或 `1`），如果该类别和样本的类别相同就是 `1`，否则是 `0`；\n- $p_{c}$ ——对于观测样本属于类别 $c$ 的预测概率。\n\n### 7.1，为什么交叉熵可以用作代价函数\n\n从数学上来理解就是，为了让学到的模型分布更接近真实数据的分布，我们需要最小化模型数据分布与训练数据之间的 `KL 散度`，而因为训练数据的分布是固定的，因此最小化 `KL 散度`等价于最小化交叉熵，而且交叉熵计算更简单，所以机器/深度学习中常用交叉熵 `cross-entroy` 作为分类问题的损失函数。\n\n### 7.2，优化算法理解\n`Adam`、`AdaGrad`、`RMSProp`优化算法具有自适应性。\n\n## 八，感受野理解\n\n感受野理解(`Receptive Field`)是指后一层神经元在前一层神经元的感受空间，也可以定义为卷积神经网络中**每层的特征图（`Feature Map`）上的像素点在原始图像中映射的区域大小**，即如下图所示：\n\n![感受野大小](../images/dl_basic/receptive_field.jpg)\n\n注意：感受野在 `CNN` 中是呈指数级增加的。小卷积核（如 `3*3`）通过多层叠加可取得与大卷积核（如 `7*7`）同等规模的感受野，此外采用小卷积核有两个优势：\n\n1. 小卷积核需多层叠加，加深了网络深度进而增强了网络容量(`model capacity`)和复杂度（`model complexity`）。\n2. 增强了网络容量的同时减少了参数个数。\n\n### 8.1，感受野大小计算\n\n计算感受野时，我们需要知道：\n> 参考 [感受野（receptive file）计算](https://www.starlg.cn/blog/2017/06/13/Receptive-File/ \"感受野（receptive file）计算\")\n\n+ 第一层卷积层的输出特征图像素的感受野的大小等于滤波器的大小；\n+ 深层卷积层的感受野大小和它之前所有层的滤波器大小和步长有关系；\n+ 计算感受野大小时，忽略了图像边缘的影响。\n\n感受野的计算方式有两种：自底向上和自顶向下（`top to down`），这里只讲解后者。正常卷积（且不带 `padding`）感受野计算公式如下：\n\n$$F(i, j-1) = (F(i, j)-1)*stride + kernel\\_size$$\n\n其中 $F(i, j)$ 表示第 `i` 层对第 `j` 层的局部感受野，所以这个公式是从上层向下层计算感受野的。仔细看这个公式会发现和反卷积输出大小计算公式一模一样，实际上感受野计算公式就是 `feature_map` 计算公式的反向推导。\n\n以下 `Python` 代码可以实现计算 `Alexnet zf-5` 和 `VGG16` 网络每层输出 `feature map` 的感受野大小，卷积核大小和输入图像尺寸默认定义好了，代码如下：\n\n```python\n# !/usr/bin/env python\n\n# [filter size, stride, padding]\nnet_struct = {'alexnet': {'net':[[11,4,0],[3,2,0],[5,1,2],[3,2,0],[3,1,1],[3,1,1],[3,1,1],[3,2,0]],\n                   'name':['conv1','pool1','conv2','pool2','conv3','conv4','conv5','pool5']},\n       'vgg16': {'net':[[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[3,1,1],\n                        [2,2,0],[3,1,1],[3,1,1],[3,1,1],[2,2,0],[3,1,1],[3,1,1],[3,1,1],[2,2,0]],\n                 'name':['conv1_1','conv1_2','pool1','conv2_1','conv2_2','pool2','conv3_1','conv3_2',\n                         'conv3_3', 'pool3','conv4_1','conv4_2','conv4_3','pool4','conv5_1','conv5_2','conv5_3','pool5']},\n       'zf-5':{'net': [[7,2,3],[3,2,1],[5,2,2],[3,2,1],[3,1,1],[3,1,1],[3,1,1]],\n               'name': ['conv1','pool1','conv2','pool2','conv3','conv4','conv5']}}\n\n\ndef outFromIn(isz, net, layernum):\n    \"\"\"\n    计算feature map大小\n    \"\"\"\n    totstride = 1\n    insize = isz\n    # for layer in range(layernum):\n    fsize, stride, pad = net[layernum]\n    outsize = (insize - fsize + 2*pad) / stride + 1\n    insize = outsize\n    totstride = totstride * stride\n    return outsize, totstride\n\ndef inFromOut(net, layernum):\n    \"\"\"\n    计算感受野receptive file大小\n    \"\"\"\n    RF = 1\n    for layer in reversed(range(layernum)):  # reversed 函数返回一个反向的迭代器\n        fsize, stride, pad = net[layer]\n        RF = ((RF -1)* stride) + fsize\n    return RF\n\nif __name__ == '__main__':\n    imsize = 224\n    feature_size = imsize\n    print (\"layer output sizes given image = %dx%d\" % (imsize, imsize))\n    \n    for net in net_struct.keys():\n        feature_size = imsize\n        print ('************net structrue name is %s**************'% net)\n        for i in range(len(net_struct[net]['net'])):\n            feature_size, stride = outFromIn(feature_size, net_struct[net]['net'], i)\n            rf = inFromOut(net_struct[net]['net'], i+1)\n            print (\"Layer Name = %s, Output size = %3d, Stride = % 3d, RF size = %3d\" % (net_struct[net]['name'][i], feature_size, stride, rf))\n```\n\n**程序输出结果如下：**\n\n```shell\nlayer output sizes given image = 224x224\n************net structrue name is alexnet**************\nLayer Name = conv1, Output size =  54, Stride =   4, RF size =  11\nLayer Name = pool1, Output size =  26, Stride =   2, RF size =  19\nLayer Name = conv2, Output size =  26, Stride =   1, RF size =  51\nLayer Name = pool2, Output size =  12, Stride =   2, RF size =  67\nLayer Name = conv3, Output size =  12, Stride =   1, RF size =  99\nLayer Name = conv4, Output size =  12, Stride =   1, RF size = 131\nLayer Name = conv5, Output size =  12, Stride =   1, RF size = 163\nLayer Name = pool5, Output size =   5, Stride =   2, RF size = 195\n************net structrue name is vgg16**************\nLayer Name = conv1_1, Output size = 224, Stride =   1, RF size =   3\nLayer Name = conv1_2, Output size = 224, Stride =   1, RF size =   5\nLayer Name = pool1, Output size = 112, Stride =   2, RF size =   6\nLayer Name = conv2_1, Output size = 112, Stride =   1, RF size =  10\nLayer Name = conv2_2, Output size = 112, Stride =   1, RF size =  14\nLayer Name = pool2, Output size =  56, Stride =   2, RF size =  16\nLayer Name = conv3_1, Output size =  56, Stride =   1, RF size =  24\nLayer Name = conv3_2, Output size =  56, Stride =   1, RF size =  32\nLayer Name = conv3_3, Output size =  56, Stride =   1, RF size =  40\nLayer Name = pool3, Output size =  28, Stride =   2, RF size =  44\nLayer Name = conv4_1, Output size =  28, Stride =   1, RF size =  60\nLayer Name = conv4_2, Output size =  28, Stride =   1, RF size =  76\nLayer Name = conv4_3, Output size =  28, Stride =   1, RF size =  92\nLayer Name = pool4, Output size =  14, Stride =   2, RF size = 100\nLayer Name = conv5_1, Output size =  14, Stride =   1, RF size = 132\nLayer Name = conv5_2, Output size =  14, Stride =   1, RF size = 164\nLayer Name = conv5_3, Output size =  14, Stride =   1, RF size = 196\nLayer Name = pool5, Output size =   7, Stride =   2, RF size = 212\n************net structrue name is zf-5**************\nLayer Name = conv1, Output size = 112, Stride =   2, RF size =   7\nLayer Name = pool1, Output size =  56, Stride =   2, RF size =  11\nLayer Name = conv2, Output size =  28, Stride =   2, RF size =  27\nLayer Name = pool2, Output size =  14, Stride =   2, RF size =  43\nLayer Name = conv3, Output size =  14, Stride =   1, RF size =  75\nLayer Name = conv4, Output size =  14, Stride =   1, RF size = 107\nLayer Name = conv5, Output size =  14, Stride =   1, RF size = 139\n```\n\n## 九，卷积和池化操作的作用\n\n> 卷积核池化的定义核过程理解是不难的，但是其作用却没有一个标准的答案，我在网上看了众多博客和魏秀参博士的书籍，总结了以下答案。\n\n卷积层和池化层的理解可参考魏秀参的《解析卷积神经网络》书籍，卷积（`convolution` ）操作的作用如下：\n\n1. `局部感知，参数共享` 的特点大大降低了网络参数，保证了网络的稀疏性。\n2. 通过卷积核的组合以及随着网络后续操作的进行，卷积操作可获取图像不同区域的不同类型特征；模型靠近底部的层提取的是局部的、高度通用的特征图，而更靠近顶部的层提取的是更加抽象的语义特征。\n\n池化/汇合（`pooling` ）操作作用如下：\n\n1. **特征不变性**（feature invariant）。汇合操作使模型更关注是否存在某些特征而不是特征具体的位置可看作是一种很强的先验，使特征学习包含某种程度自由度，能容忍一些特征微小的位移。\n2. **特征降维**。由于汇合操作的降采样作用，汇合结果中的一个元素对应于原输入数据的一个子区域（sub-region），因此汇合相当于在空间范围内做了维度约减（spatially dimension reduction），从而使模型可以抽取更广范围的特征。同时减小了下一层输入大小，进而减小计算量和参数个数。\n3. 在一定程度上能**防止过拟合（overfitting）**，更方便优化。\n\n### 参考资料\n\n魏秀参-《解析卷积神经网络》\n\n## 十，卷积层与全连接层的区别\n\n+ 卷积层学习到的是局部模式（对于图像，学到的就是在输入图像的二维小窗口中发现的模式）\n+ 全连接层学习到的是全局模式（全局模式就算设计所有像素）\n\n## 十一，CNN 权值共享问题\n\n首先**权值共享就是滤波器共享**，滤波器的参数是固定的，即是用相同的滤波器去扫一遍图像，提取一次特征特征，得到feature map。在卷积网络中，学好了一个滤波器，就相当于掌握了一种特征，这个滤波器在图像中滑动，进行特征提取，然后所有进行这样操作的区域都会被采集到这种特征，就好比上面的水平线。\n\n## 十二，CNN 结构特点\n\n典型的用于分类的CNN主要由**卷积层+激活函数+池化层**组成，最后用全连接层输出。卷积层负责提取图像中的局部特征；池化层用来大幅降低参数量级(降维)；全连接层类似传统神经网络的部分，用来输出想要的结果。\n\n`CNN` 具有局部连接、权值共享、池化操作(简单说就是下采样)和多层次结构的特点。\n\n+ 局部连接使网络可以提取数据的局部特征。\n+ 权值共享大大降低了网络的训练难度，一个Filter只提取一个特征，在整个图片（或者语音／文本） 中进行卷积。\n+ 池化操作与多层次结构一起，实现了数据的降维，将低层次的局部特征组合成为较高层次的特征，从而对整个图片进行表示。\n+ 卷积神经网络学到的模式具有平移不变性（`translation invariant`），且可以学到模式的空间层次结构。\n\n### Reference\n\n[(二)计算机视觉四大基本任务(分类、定位、检测、分割](https://zhuanlan.zhihu.com/p/31727402 \"(二)计算机视觉四大基本任务(分类、定位、检测、分割\")\n\n## 十三，深度特征的层次性\n\n卷积操作可获取图像区域不同类型特征，而汇合等操作可对这些特征进行融合和抽象，随着若干卷积、汇合等操作的堆叠，各层得到的深度特征逐渐从泛化特征（如边缘、纹理等）过渡到高层语义表示（躯干、头部等模式）。\n\n## 十四，什么样的数据集不适合深度学习\n\n+ 数据集太小，数据样本不足时，深度学习相对其它机器学习算法，没有明显优势。\n+ 数据集没有局部相关特性，目前深度学习表现比较好的领域主要是图像／语音／自然语言处理等领域，这些领域的一个共性是局部相关性。图像中像素组成物体，语音信号中音位组合成单词，文本数据中单词组合成句子，这些特征元素的组合一旦被打乱，表示的含义同时也被改变。对于没有这样的局部相关性的数据集，不适于使用深度学习算法进行处理。举个例子：预测一个人的健康状况，相关的参数会有年龄、职业、收入、家庭状况等各种元素，将这些元素打乱，并不会影响相关的结果。\n\n## 十五，什么造成梯度消失问题\n\n+ 神经网络的训练中，通过改变神经元的权重，使网络的输出值尽可能逼近标签以降低误差值，训练普遍使用BP算法，核心思想是，计算出输出与标签间的损失函数值，然后计算其相对于每个神经元的梯度，进行权值的迭代。\n+ 梯度消失会造成权值更新缓慢，模型训练难度增加。造成梯度消失的一个原因是，许多激活函数将输出值挤压在很小的区间内，在激活函数两端较大范围的定义域内梯度为0，造成学习停止。\n\n## 十六，Overfitting 和 Underfitting 问题\n\n### 16.1，过拟合问题怎么解决\n\n首先所谓过拟合，指的是一个模型过于复杂之后，它可以很好地“记忆”每一个训练数据中随机噪音的部分而忘记了去“训练”数据中的通用趋势。**训练好后的模型过拟合具体表现在：模型在训练数据上损失函数较小，预测准确率较高；但是在测试数据上损失函数比较大，预测准确率较低**。解决办法如下：\n\n+ `数据增强`, 增加数据多样性;\n+ 正则化策略：如 Parameter Norm Penalties(参数范数惩罚), `L1, L2正则化`;\n+ `dropout`;\n+ 模型融合, 比如Bagging 和其他集成方法;\n+ `BN` ,batch normalization;\n+ Early Stopping(提前终止训练)。\n\n### 16.2，如何判断深度学习模型是否过拟合\n\n1. 首先将训练数据划分为训练集和验证集，`80%` 用于训练集，`20%` 用于验证集（训练集和验证集一定不能相交）；训练都时候每隔一定 `Epoch` 比较验证集但指标和训练集是否一致，如果不一致，并且变坏了，那么意味着过拟合。\n2. 用学习曲线 `learning curve` 来判别过拟合，参考此[博客](https://blog.csdn.net/aliceyangxi1987/article/details/73598857 \"博客\")。\n\n### 16.3，欠拟合怎么解决\n\n`underfitting` 欠拟合的表现就是模型不收敛，原因有很多种，这里以神经网络拟合能力不足问题给出以下参考解决方法：\n\n+ 寻找最优的权重初始化方案：如 He正态分布初始化 `he_normal`，深度学习框架都内置了很多权重初始化方法；\n+ 使用适当的激活函数：卷积层的输出使用的激活函数一般为 `ReLu`，循环神经网络中的循环层使用的激活函数一般为 `tanh`，或者 `ReLu`；\n+ 选择合适的优化器和学习速率：`SGD` 优化器速度慢但是会达到最优.\n\n### 16.4，如何判断模型是否欠拟合\n\n神级网络欠拟合的特征就是模型训练了**足够长**但时间后, `loss` 值依然很大甚至与初始值没有太大区别，且精度很低，测试集亦如此。根据我的总结，原因一般有以下几种：\n\n+ 神经网络的拟合能力不足；\n+ 网络配置的问题；\n+ 数据集配置的问题；\n+ 训练方法出错（初学者经常碰到，原因千奇百怪）。\n\n## 十七，L1 和 L2 区别\n\nL1 范数（`L1 norm`）是指向量中各个元素绝对值之和，也有个美称叫“稀疏规则算子”（Lasso regularization）。 比如 向量 A=[1，-1，3]， 那么 A 的 L1 范数为 |1|+|-1|+|3|。简单总结一下就是：\n\n+ L1 范数: 为向量 x 各个元素绝对值之和。\n+ L2 范数: 为向量 x 各个元素平方和的 1/2 次方，L2 范数又称 Euclidean 范数或 Frobenius 范数\n+ Lp 范数: 为向量 x 各个元素绝对值 $p$ 次方和的 $1/p$ 次方.\n\n在支持向量机学习过程中，L1 范数实际是一种对于成本函数求解最优的过程，因此，L1 范数正则化通过向成本函数中添加 L1 范数，使得学习得到的结果满足稀疏化，从而方便人类提取特征。\n\n`L1` 范数可以使权值参数稀疏，方便特征提取。 L2 范数可以防止过拟合，提升模型的泛化能力。\n\n## 十八，TensorFlow计算图概念\n\n`Tensorflow` 是一个通过计算图的形式来表述计算的编程系统，计算图也叫数据流图，可以把计算图看做是一种有向图，Tensorflow 中的每一个计算都是计算图上的一个节点，而节点之间的边描述了计算之间的依赖关系。\n\n## 十九，BN（批归一化）的作用\n\n在神经网络中间层也进行归一化处理，使训练效果更好的方法，就是批归一化Batch Normalization（BN）。\n\n(1). **可以使用更高的学习率**。如果每层的 `scale` 不一致，实际上每层需要的学习率是不一样的，同一层不同维度的 scale 往往也需要不同大小的学习率，通常需要使用最小的那个学习率才能保证损失函数有效下降，Batch Normalization 将每层、每维的 scale 保持一致，那么我们就可以直接使用较高的学习率进行优化。\n\n(2). **移除或使用较低的 `dropout`**。 dropout 是常用的防止 overfitting 的方法，而导致 overfitting 的位置往往在数据边界处，如果初始化权重就已经落在数据内部，overfitting现象就可以得到一定的缓解。论文中最后的模型分别使用10%、5%和0%的dropout训练模型，与之前的 40%-50% 相比，可以大大提高训练速度。\n\n(3). **降低 L2 权重衰减系数**。 还是一样的问题，边界处的局部最优往往有几维的权重（斜率）较大，使用 L2 衰减可以缓解这一问题，现在用了 Batch Normalization，就可以把这个值降低了，论文中降低为原来的 5 倍。\n\n(4). **代替Local Response Normalization层**。 由于使用了一种 Normalization，再使用 LRN 就显得没那么必要了。而且 LRN 实际上也没那么 work。\n\n(5). **Batch Normalization调整了数据的分布，不考虑激活函数，它让每一层的输出归一化到了均值为0方差为1的分布**，这保证了梯度的有效性，可以解决反向传播过程中的梯度问题。目前大部分资料都这样解释，比如 BN 的原始论文认为的缓解了 Internal Covariate Shift(ICS) 问题。\n\n关于训练阶段和推理阶段 `BN` 的不同可以参考 [Batch Normalization详解](https://www.cnblogs.com/shine-lee/p/11989612.html \"Batch Normalization详解\") 。\n\n## 二十，什么是梯度消失和爆炸\n\n+ 梯度消失是指在深度学习训练的过程中，梯度随着 `BP` 算法中的链式求导逐层传递逐层减小，最后趋近于0，导致对某些层的训练失效；\n+ 梯度爆炸与梯度消失相反，梯度随着 `BP` 算法中的链式求导逐层传递逐层增大，最后趋于无穷，导致某些层无法收敛；\n\n> 在反向传播过程中需要对激活函数进行求导，如果导数大于 1，那么随着网络层数的增加，梯度更新将会朝着指数爆炸的方式增加这就是梯度爆炸。同样如果导数小于 1，那么随着网络层数的增加梯度更新信息会朝着指数衰减的方式减少这就是梯度消失。\n\n### 梯度消失和梯度爆炸产生的原因\n\n**出现梯度消失和梯度爆炸的问题主要是因为参数初始化不当以及激活函数选择不当造成的**。其根本原因在于反向传播训练法则，属于先天不足。当训练较多层数的模型时，一般会出现梯度消失问题（gradient vanishing problem）和梯度爆炸问题（gradient exploding problem）。注意在反向传播中，当网络模型层数较多时，梯度消失和梯度爆炸是不可避免的。\n\n**深度神经网络中的梯度不稳定性，根本原因在于前面层上的梯度是来自于后面层上梯度的乘积**。当存在过多的层次时，就出现了内在本质上的不稳定场景。前面的层比后面的层梯度变化更小，故变化更慢，故引起了梯度消失问题。前面层比后面层梯度变化更快，故引起梯度爆炸问题。\n\n### 如何解决梯度消失和梯度爆炸问题\n\n常用的有以下几个方案：\n\n+ 预训练模型 + 微调\n+ 梯度剪切 + 正则化\n+ `relu、leakrelu` 等激活函数\n+ `BN` 批归一化\n+ 参数标准初始化函数如 `xavier`\n+ `CNN` 中的残差结构\n+ `LSTM` 结构\n\n## 二十一，RNN循环神经网络理解\n\n循环神经网络（recurrent neural network, RNN）, 主要应用在语音识别、语言模型、机器翻译以及时序分析等问题上。\n**在经典应用中，卷积神经网络在不同的空间位置共享参数，循环神经网络是在不同的时间位置共享参数，从而能够使用有限的参数处理任意长度的序列。**\n\n`RNN` 可以看做作是同一神经网络结构在时间序列上被复制多次的结果，这个被复制多次的结构称为循环体，如何设计循环体的网络结构是 RNN 解决实际问题的关键。RNN 的输入有两个部分，一部分为上一时刻的状态，另一部分为当前时刻的输入样本。\n\n## 二十二，训练过程中模型不收敛，是否说明这个模型无效，导致模型不收敛的原因\n\n不一定。导致模型不收敛的原因有很多种可能，常见的有以下几种：\n\n+ 没有对数据做归一化。\n+ 没有检查过你的结果。这里的结果包括预处理结果和最终的训练测试结果。\n+ 忘了做数据预处理。\n+ 忘了使用正则化。\n+ Batch Size 设的太大。\n+ 学习率设的不对。\n+ 最后一层的激活函数用的不对。\n+ 网络存在坏梯度。比如 ReLu 对负值的梯度为 0，反向传播时，0 梯度就是不传播。\n+ 参数初始化错误。\n+ 网络太深。隐藏层神经元数量错误。\n+ 更多回答，参考此[链接](http://theorangeduck.com/page/neural-network-not-working \"链接\")。\n\n## 二十三，VGG 使用 2 个 3*3 卷积的优势\n\n(1). **减少网络层参数**。用两个 3\\*3 卷积比用 1 个 5\\*5 卷积拥有更少的参数量，只有后者的 2∗3∗3/5∗5=0.72。但是起到的效果是一样的，两个 3×3 的卷积层串联相当于一个 5×5 的卷积层，感受野的大小都是 5×5，即 1 个像素会跟周围 5×5 的像素产生关联。把下图当成动态图看，很容易看到两个 3×3 卷积层堆叠（没有空间池化）有 5×5 的有效感受野。\n\n![2个３*3卷积层](../images/dl_basic/3x3conv.png)\n\n(2). **更多的非线性变换**。2 个 3×3 卷积层拥有比 1 个 5×5 卷积层更多的非线性变换（前者可以使用两次 ReLU 激活函数，而后者只有一次），使得卷积神经网络对特征的学习能力更强。\n\n***paper中给出的相关解释***：三个这样的层具有 7×7 的有效感受野。那么我们获得了什么？例如通过使用三个 3×3 卷积层的堆叠来替换单个 7×7 层。首先，我们结合了三个非线性修正层，而不是单一的，这使得决策函数更具判别性。其次，我们减少参数的数量：假设三层 3×3 卷积堆叠的输入和输出有 C 个通道，堆叠卷积层的参数为 3×(3×3C) = `27C` 个权重；同时，单个 7×7 卷积层将需要 7×7×C = `49C` 个参数，即参数多 81％。这可以看作是对 7×7 卷积滤波器进行正则化，迫使它们通过 3×3 滤波器（在它们之间注入非线性）进行分解。\n\n**此回答可以参考 TensorFlow 实战 p110，网上很多回答都说的不全**。\n\n### 23.1，1*1 卷积的主要作用\n\n+ **降维（ dimension reductionality ）**。比如，一张500 \\* 500且厚度depth为100 的图片在20个filter上做1\\*1的卷积，那么结果的大小为500\\*500\\*20。\n+ **加入非线性**。卷积层之后经过激励层，1\\*1的卷积在前一层的学习表示上添加了非线性激励（ non-linear activation ），提升网络的表达能力；\n\n## 二十四，Relu比Sigmoid效果好在哪里？\n\n`Sigmoid` 函数公式如下：\n$\\sigma (x)=\\frac{1}{1+exp(-x)}$\n\nReLU激活函数公式如下：\n$$f(x) = max(x, 0) = \n\\begin{cases}\nx,  & \\text{if $x$ $\\geq$ 0} \\\\\n0, & \\text{if $x$ < 0}\n\\end{cases}$$\n\nReLU 的输出要么是 0, 要么是输入本身。虽然方程简单，但实际上效果更好。在网上看了很多版本的解释，有从程序实例分析也有从数学上分析，我找了个相对比较直白的回答，如下：\n\n1. `ReLU` 函数计算简单，可以减少很多计算量。反向传播求误差梯度时，涉及除法，计算量相对较大，采用 `ReLU` 激活函数，可以节省很多计算量；\n2. **避免梯度消失问题**。对于深层网络，`sigmoid` 函数反向传播时，很容易就会出现梯度消失问题（在sigmoid接近饱和区时，变换太缓慢，导数趋于 0，这种情况会造成信息丢失），从而无法完成深层网络的训练。\n3. 可以缓解过拟合问题的发生。`ReLU` 会使一部分神经元的输出为 0，这样就造成了网络的稀疏性，并且减少了参数的相互依存关系，缓解了过拟合问题的发生。\n4. 相比 `sigmoid` 型函数，`ReLU` 函数有助于随机梯度下降方法收敛。\n\n### 参考链接\n\n[ReLU为什么比Sigmoid效果好](https://www.twblogs.net/a/5c2dd30fbd9eee35b21c4337/zh-cn \"ReLU为什么比Sigmoid效果好\")\n\n## 二十五，神经网络中权值共享的理解\n\n权值(权重)共享这个词是由 LeNet5 模型提出来的。以 CNN 为例，在对一张图偏进行卷积的过程中，使用的是同一个卷积核的参数。\n比如一个 3×3×1 的卷积核，这个卷积核内 9 个的参数被整张图共享，而不会因为图像内位置的不同而改变卷积核内的权系数。说的再直白一些，就是用一个卷积核不改变其内权系数的情况下卷积处理整张图片（当然CNN中每一层不会只有一个卷积核的，这样说只是为了方便解释而已）。\n\n### 参考资料\n\n[如何理解CNN中的权值共享](https://blog.csdn.net/chaipp0607/article/details/73650759 \"如何理解CNN中的权值共享\")\n\n## 二十六，对 fine-tuning(微调模型的理解)，为什么要修改最后几层神经网络权值？\n\n使用预训练模型的好处，在于利用训练好的SOTA模型权重去做特征提取，可以节省我们训练模型和调参的时间。\n\n至于为什么只微调最后几层神经网络权重，是因为：\n\n(1). CNN 中更靠近底部的层（定义模型时先添加到模型中的层）编码的是更加通用的可复用特征，而更靠近顶部的层（最后添加到模型中的层）编码的是更专业业化的特征。微调这些更专业化的特征更加有用，它更代表了新数据集上的有用特征。\n(2). 训练的参数越多，过拟合的风险越大。很多SOTA模型拥有超过千万的参数，在一个不大的数据集上训练这么多参数是有过拟合风险的，除非你的数据集像Imagenet那样大。\n\n### 参考资料\n\nPython深度学习p127.\n\n## 二十七，什么是 dropout?\n\n+ dropout可以防止过拟合，dropout简单来说就是：我们在前向传播的时候，**让某个神经元的激活值以一定的概率 p 停止工作**，这样可以使模型的泛化性更强，因为它不会依赖某些局部的特征。\n+ `dropout`效果跟`bagging`效果类似（bagging是减少方差variance，而boosting是减少偏差bias）\n+ 加入dropout会使神经网络训练时间边长，模型预测时不需要dropout，记得关掉。\n\n![dropou直观展示](../images/dl_basic/dropout.jpg)\n\n### 27.1，dropout具体工作流程\n\n以 标准神经网络为例，正常的流程是：我们首先把输入数据x通过网络前向传播，然后把误差反向传播一决定如何更新参数让网络进行学习。使用dropout之后，过程变成如下：\n\n1，首先随机（临时）删掉网络中一半的隐藏神经元，输入输出神经元保持不变（图3中虚线为部分临时被删除的神经元）；\n\n2，然后把输入x通过修改后的网络进行前向传播计算，然后把得到的损失结果通过修改的网络反向传播。一小批训练样本执行完这个过程后，在没有被删除的神经元上按照随机梯度下降法更新对应的参数（w，b）；\n\n3，然后重复这一过程：\n\n+ 恢复被删掉的神经元（此时被删除的神经元保持原样没有更新w参数，而没有被删除的神经元已经有所更新）;\n+ 从隐藏层神经元中随机选择一个一半大小的子集临时删除掉（同时备份被删除神经元的参数）;\n+ 对一小批训练样本，先前向传播然后反向传播损失并根据随机梯度下降法更新参数（w，b） （没有被删除的那一部分参数得到更新，删除的神经元参数保持被删除前的结果）。\n\n### 27.2，dropout在神经网络中的应用\n\n(1). 在训练模型阶段\n\n不可避免的，在训练网络中的每个单元都要添加一道概率流程，标准网络和带有dropout网络的比较图如下所示：\n\n![dropout在训练阶段](../images/dl_basic/dropout2.png)\n\n(2). 在测试模型阶段\n\n预测模型的时候，输入是当前输入，每个神经单元的权重参数要乘以概率p。\n\n![dropout在测试模型时](../images/dl_basic/infer_dropout.jpg)\n\n### 27.3，如何选择dropout 的概率\n\ninput 的 dropout 概率推荐是 0.8， hidden layer 推荐是0.5， 但是也可以在一定的区间上取值。（All dropout nets use p = 0.5 for hidden units and p = 0.8 for input units.）\n\n### 参考资料\n\n1. [Dropout:A Simple Way to Prevent Neural Networks from Overfitting]\n2. [深度学习中Dropout原理解析](https://zhuanlan.zhihu.com/p/38200980 \"深度学习中Dropout原理解析\")\n\n## 二十八，HOG 算法原理描述\n\n**方向梯度直方图（Histogram of Oriented Gradient, HOG）特征**是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。在深度学习取得成功之前，Hog特征结合SVM分类器被广泛应用于图像识别中，在行人检测中获得了较大的成功。\n\n### HOG特征原理\n\n`HOG` 的核心思想是所检测的局部物体外形能够被光强梯度或边缘方向的分布所描述。通过将整幅图像分割成小的连接区域（称为cells），每个 `cell` 生成一个方向梯度直方图或者 cell 中pixel 的边缘方向，这些直方图的组合可表示（所检测目标的目标）描述子。\n\n为改善准确率，局部直方图可以通过计算图像中一个较大区域(称为block)的光强作为 `measure` 被对比标准化，然后用这个值(measure)归一化这个 block 中的所有 cells 。这个归一化过程完成了更好的照射/阴影不变性。与其他描述子相比，HOG 得到的描述子保持了几何和光学转化不变性（除非物体方向改变）。因此HOG描述子尤其适合人的检测。\n\nHOG 特征提取方法就是将一个image：\n\n1. 灰度化（将图像看做一个x,y,z（灰度）的三维图像）\n2. 划分成小 `cells`（2*2）\n3. 计算每个 cell 中每个 pixel 的 gradient（即 orientation）\n4. 统计每个 cell 的梯度直方图（不同梯度的个数），即可形成每个 cell 的 descriptor。\n\n### HOG特征检测步骤\n\n![HOG特征检测步骤](../images/dl_basic/hog_process.jpg)\n\n**总结**：颜色空间归一化——>梯度计算——>梯度方向直方图——>重叠块直方图归一化———>HOG特征\n\n### 参考资料\n[HOG特征检测－简述](https://blog.csdn.net/liyuqian199695/article/details/53835989 \"HOG特征检测－简述\")\n\n## 二十九，激活函数\n\n### 29.1，激活函数的作用\n\n+ **激活函数实现去线性化**。激活函数的引入是为了增加整个网络的表达能力（引入非线性因素），否则若干线形层的堆叠仍然只能起到线性映射的作用，无法形成复杂的函数。如果将每一个神经元（也就是神经网络的节点）的输出通过一个非线性函数，那么整个神经网络的模型也就不再是线性的了，这个非线性函数就是激活函数。\n+ 激活函数可以把**当前特征空间通过一定的线性映射转换到另一个空间**，让数据能够更好的被分类。\n+ 激活函数对模型学习、理解非常复杂和非线性的函数具有重要作用。\n\n### 29.2，常见的激活函数\n\n激活函数也称非线性映射函数，常见的激活函数有：`ReLU` 函数、`sigmoid` 函数、`tanh` 函数等，其计算公式如下：\n\n+ ReLU函数：$f(x)=max(x,0)$\n+ sigmoid函数：$f(x)=\\frac{1}{1+e^{-x}}$\n+ tanh函数：$f(x)=\\frac{1+e^{-2x}}{1+e^{-2x}}=\\frac{2}{1+e^{-2x}}-1 = 2sigmoid(2x)-1$\n\n### 29.3，激活函数理解及函数梯度图\n\n1. `Sigmoid` 激活函数，可用作分类任务的输出层。可以看出经过sigmoid激活函数后，模型输出的值域被压缩到 `0到1` 区间（这和概率的取值范围一致），这正是分类任务中sigmoid很受欢迎的原因。\n\n![sigmoid激活函数及梯度图](../images/dl_basic/sigmoid_act.png)\n\n1. `tanh(x)` 型函数是在 `sigmoid` 型函数基础上为解决均值问题提出的激活函数。tanh 的形状和 sigmoid 类似，只不过tanh将“挤压”输入至区间(-1, 1)。至于梯度，它有一个大得多的峰值1.0（同样位于z = 0处），但它下降得更快，当|x|的值到达 3 时就已经接近零了。这是所谓梯度消失（vanishing gradients）问题背后的原因，会导致网络的训练进展变慢。\n\n![tanh函数及梯度图](../images/dl_basic/tanh_and_gradient.png)\n\n1. `ReLU` 处理了sigmoid、tanh中常见的梯度消失问题，同时也是计算梯度最快的激励函数。但是，ReLU函数也有自身缺陷，即在 x < 0 时，梯度便为 y。换句话说，对于小于 y 的这部分卷积结果响应，它们一旦变为负值将再无法影响网络训练——这种现象被称作“死区\"。\n\n![Relu函数及梯度图](../images/dl_basic/relu_and_gradient.jpg)\n\n## 三十，卷积层和池化层有什么区别\n\n1. **卷积层有参数，池化层没有参数**；\n2. **经过卷积层节点矩阵深度会改变**。池化层不会改变节点矩阵的深度，但是它可以缩小节点矩阵的大小。\n\n## 三十一，卷积层和池化层参数量计算\n\n参考 [神经网络模型复杂度分析](神经网络模型复杂度分析.md \"神经网络模型复杂度分析\") 文章。\n\n## 三十二，神经网络为什么用交叉熵损失函数\n\n判断一个输出向量和期望的向量有多接近，交叉熵（`cross entroy`）是常用的评判方法之一。交叉熵刻画了两个概率分布之间的距离，是分类问题中使用比较广泛的一种损失函数。给定两个概率分布 `p` 和 `q` ，通过 `q` 来表示 `p` 的交叉熵公式为：$H(p,q)=−∑p(x)logq(x)$\n\n## 三十三，数据增强方法有哪些\n\n常用数据增强方法：\n\n+ 翻转：`Fliplr,Flipud`。不同于旋转180度，这是类似镜面的翻折，跟人在镜子中的映射类似，常用水平、上下镜面翻转。\n+ 旋转：`rotate`。顺时针/逆时针旋转，最好旋转 `90-180` 度，否则会出现边缘缺失或者超出问题，如旋转 `45` 度。\n+ 缩放：`zoom`。图像可以被放大或缩小，imgaug库可用Scal函数实现。\n+ 裁剪：`crop`。一般叫随机裁剪，操作步骤是：随机从图像中选择一部分，然后降这部分图像裁剪出来，然后调整为原图像的大小。根本上理解，图像crop就是指从图像中移除不需要的信息，只保留需要的部分\n+ 平移：`translation`。平移是将图像沿着x或者y方向（或者两个方向）移动。我们在平移的时候需对背景进行假设，比如说假设为黑色等等，因为平移的时候有一部分图像是空的，由于图片中的物体可能出现在任意的位置，所以说平移增强方法十分有用。\n+ 放射变换：`Affine`。包含：平移(`Translation`)、旋转(`Rotation`)、放缩(`zoom`)、错切(`shear`)。\n+ 添加噪声：过拟合通常发生在神经网络学习高频特征的时候，为消除高频特征的过拟合，可以随机加入噪声数据来消除这些高频特征。`imgaug` 图像增强库使用 `GaussianBlur`函数。\n+ 亮度、对比度增强：这是图像色彩进行增强的操作\n+ 锐化：`Sharpen`。imgaug库使用Sharpen函数。\n\n### 33.1，离线数据增强和在线数据增强有什么区别?\n\n数据增强分两类，一类是**离线增强**，一类是**在线增强**：\n\n1. 离线增强 ： 直接对硬盘上的数据集进行处理，并保存增强后的数据集文件。数据的数目会变成增强因子 x 原数据集的数目 ，这种方法常常用于数据集很小的时候\n2. 在线增强 ： 这种增强的方法用于，获得 batch 数据之后，然后对这个batch的数据进行增强，如旋转、平移、翻折等相应的变化，由于有些数据集不能接受线性级别的增长，这种方法长用于大的数据集，很多机器学习框架已经支持了这种数据增强方式，并且可以使用GPU优化计算。\n`\n### Reference\n\n[深度学习中的数据增强](https://blog.csdn.net/zhelong3205/article/details/81810743 \"深度学习中的数据增强\")\n\n## 三十四，ROI Pooling替换为ROI Align的效果，及各自原理\n\n`faster rcnn` 将 `roi pooling` 替换为 `roi align` 效果有所提升。\n\n### ROI Pooling原理\n\n`RPN`　生成的 `ROI` 区域大小是对应与输入图像大小（每个roi区域大小各不相同），为了能够共享权重，所以需要将这些 `ROI`　映射回特征图上，并固定大小。ROI Pooling操作过程如下图：\n\n![roi pooling](../images/dl_basic/roi_pooling.png)\n\n**`ROI Pooling` 具体操作如下：**\n\n1. `Conv layers` 使用的是 `VGG16`，`feat_stride=32`(即表示，经过网络层后图片缩小为原图的 1/32),原图 $800\\times 800$,最后一层特征图 `feature map` 大小: $25\\times 25$\n2. 假定原图中有一 `region proposal`，大小为 $665\\times 665$，这样，映射到特征图中的大小：$665/32=20.78$，即大小为 $20.78\\times 20.78$，源码中，在计算的时候会进行取整操作，于是，进行所谓的第一次量化，即映射的特征图大小为20*20；\n3. 假定 `pooled_w=7、pooled_h=7`，即 `pooling` 后固定成 $7\\times 7$ 大小的特征图，所以，将上面在 `feature map`上映射的 $20\\times 20$ 的 `region proposal` 划分成 `49`个同等大小的小区域，每个小区域的大小 $20/7=2.86$,，即 $2.86\\times 2.86$，此时，进行第二次量化，故小区域大小变成 $2\\times 2$；\n4. 每个 $2\\times 2$ 的小区域里，取出其中最大的像素值，作为这一个区域的‘代表’，这样，49 个小区域就输出 49 个像素值，组成 $7\\times 7$ 大小的 `feature map`。\n\n所以，通过上面可以看出，经过**两次量化，即将浮点数取整**，原本在特征图上映射的 $20\\times 20$ 大小的 `region proposal`，偏差成大小为 $7\\times 7$ 的，这样的像素偏差势必会对后层的回归定位产生影响。所以，产生了后面的替代方案: `RoiAlign`。\n\n### ROI Align原理\n\n`ROI Align` 的工作原理如下图。\n\n![roi align](../images/dl_basic/roi_align.png)\n\n`ROI Align` 是在 `Mask RCNN` 中使用以便使生成的候选框`region proposal` 映射产生固定大小的 `feature map` 时提出的，根据上图，有着类似的映射：\n\n1. `Conv layers` 使用的是 `VGG16`，`feat_stride=32` (即表示，经过网络层后图片缩小为原图的1/32),原图 $800\\times 800$，最后一层特征图feature map大小: $25*\\times 25$;\n2. 假定原图中有一region proposal，大小为 $665*\\times 665$，这样，映射到特征图中的大小：$665/32=20.78$，即大小为 $20.78\\times 20.78$，此时，没有像 `RoiPooling` 那样就行取整操作，保留浮点数;\n3. 假定 `pooled_w=7,pooled_h=7`，即 `pooling` 后固定成 $7\\times 7$ 大小的特征图，所以，将在 `feature map` 上映射的 $20.78\\times 20.78$ 的 `region proposal` 划分成 49 个同等大小的小区域，每个小区域的大小 $20.78/7=2.97$,即 $2.97\\times 2.97$;\n4. 假定采样点数为 `4`，即表示，对于每个 $2.97\\times 2.97$ 的小区域，平分四份，每一份取其中心点位置，而中心点位置的像素，采用双线性插值法进行计算，这样，就会得到四个点的像素值，如下图:\n\n![roi align双线性插值](../images/dl_basic/roi_align_linear_interpolation.png)\n\n上图中，四个红色叉叉`‘×’`的像素值是通过双线性插值算法计算得到的。\n\n最后，取四个像素值中最大值作为这个小区域(即：$2.97\\times 2.97$ 大小的区域)的像素值，如此类推，同样是 `49` 个小区域得到 `49` 个像素值，组成 $7\\times 7$ 大小的 `feature map`。\n\n### RoiPooling 和 RoiAlign 总结\n\n总结：知道了 `RoiPooling` 和 `RoiAlign` 实现原理，在以后的项目中可以根据实际情况进行方案的选择；对于检测图片中大目标物体时，两种方案的差别不大，而如果是图片中有较多小目标物体需要检测，则优先选择 `RoiAlign`，更精准些。\n\n### Reference\n[RoIPooling、RoIAlign笔记](https://www.cnblogs.com/wangyong/p/8523814.html \"RoIPooling、RoIAlign笔记\")\n\n## 三十五，CNN的反向传播算法推导\n\n+ [四张图彻底搞懂CNN反向传播算法（通俗易懂）](https://zhuanlan.zhihu.com/p/81675803 \"四张图彻底搞懂CNN反向传播算法（通俗易懂）\")\n+ [反向传播算法推导过程（非常详细）](https://zhuanlan.zhihu.com/p/79657669 \"反向传播算法推导过程（非常详细）\")\n\n## 三十六，Focal Loss 公式\n\n为了解决正负样本不平衡的问题，我们通常会在交叉熵损失的前面加上一个参数 $\\alpha$ ，即:\n$$\nCE = \\left\\{\\begin{matrix}\n-\\alpha log(p), & if \\quad y=1\\\\ \n-(1-\\alpha)log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.\n$$\n\n尽管 $\\alpha$  平衡了正负样本，但对难易样本的不平衡没有任何帮助。因此，`Focal Loss` 被提出来了，即：\n\n$$\nCE = \\left\\{\\begin{matrix}\n-\\alpha (1-p)^\\gamma log(p), & if \\quad y=1\\\\ \n-(1-\\alpha) p^\\gamma log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.\n$$\n\n实验表明 $\\gamma$ 取 `2`, $\\alpha$ 取 `0.25` 的时候效果最佳。\n\n+ `Focal loss` 成功地解决了在单阶段目标检测时，**正负样本区域极不平衡**而目标检测 `loss` 易被大批量负样本所左右的问题。\n+ `RetinaNet` 达到更高的精度的原因不是网络结构的创新，而是损失函数的创新！\n\n## 三十七，快速回答\n\n### 37.1，为什么 Faster RCNN、Mask RCNN 需要使用 ROI Pooling、ROI Align?\n\n为了使得最后面的两个全连接层能够共享 `conv layers(VGG/ResNet)` 权重。在所有的 `RoIs` 都被 `pooling` 成（512×7×7）的`feature map`后，将它 `reshape` 成一个一维的向量，就可以利用 `VGG16` 的预训练的权重来初始化前两层全连接。\n\n### 37.2，softmax公式\n\n$$softmax(y)_{i} = \\frac{e^{y_{i}}}{\\sum_{j=1}^{n}e^{y_j}}$$\n\n### 37.3，上采样方法总结\n\n上采样大致被总结成了三个类别：\n\n1. 基于线性插值的上采样：最近邻算法（`nearest`）、双线性插值算法（`bilinear`）、双三次插值算法（`bicubic`）等，这是传统图像处理方法。\n2. 基于深度学习的上采样（转置卷积，也叫反卷积 `Conv2dTranspose2d`等）\n3. `Unpooling` 的方法（简单的补零或者扩充操作）\n\n> 计算效果：最近邻插值算法 < 双线性插值 < 双三次插值。计算速度：最近邻插值算法 > 双线性插值 > 双三次插值\n\n### 37.4，移动端深度学习框架知道哪些，用过哪些？\n\n知名的有TensorFlow Lite、小米MACE、腾讯的 ncnn 等，目前都没有用过。\n\n### 37.5，如何提升网络的泛化能力\n\n和防止模型过拟合的方法类似，另外还有模型融合方法。\n\n### 37.6，BN算法，为什么要在后面加伽马和贝塔，不加可以吗？\n\n最后的 `scale and shift` 操作则是为了让因训练所需而“刻意”加入的BN能够有可能还原最初的输入。不加也可以。\n\n### 37.7，验证集和测试集的作用\n+ 验证集是在训练过程中用于检验模型的训练情况，**从而确定合适的超参数**；\n+ 测试集是在模型训练结束之后，**测试模型的泛化能力**。\n\n## 三十八，交叉验证的理解和作用\n\n参考知乎文章 [N折交叉验证的作用（如何使用交叉验证）](https://zhuanlan.zhihu.com/p/113623623 \"N折交叉验证的作用（如何使用交叉验证）\")，我的理解之后补充。\n\n## 三十九，介绍一下NMS和IOU的原理\n\nNMS全称是非极大值抑制，顾名思义就是抑制不是极大值的元素。在目标检测任务中，通常在解析模型输出的预测框时，预测目标框会非常的多，其中有很多重复的框定位到了同一个目标，NMS 的作用就是用来除去这些重复框，从而获得真正的目标框。而 NMS 的过程则用到了 IOU，IOU 是一种用于衡量真实和预测之间相关度的标准，相关度越高，该值就越高。IOU 的计算是两个区域重叠的部分除以两个区域的集合部分，简单的来说就是交集除以并集。\n\n在 NMS 中，首先对预测框的置信度进行排序，依次取置信度最大的预测框与后面的框进行 IOU 比较，当 IOU 大于某个阈值时，可以认为两个预测框框到了同一个目标，而置信度较低的那个将会被剔除，依次进行比较，最终得到所有的预测框。\n\n## 四十，评估权重通道的重要性\n\n在卷积神经网络（CNN）和大语言模型（LLM）中，通道（channel/embedding 维度）是特征图和 `Embedding` 张量的基本组成部分，评估通道的重要性有助于进行模型剪枝和优化。常用的方法包括：\n- 基于 `L1` 范数的剪枝：计算每个通道权重的 L1 范数，并根据其大小进行排序，剪除 L1 范数较小的通道。\n- 基于 `L2` 范数的剪枝：类似地，计算每个通道权重的 L2 范数，剪除 L2 范数较小的通道。\n- 基于权重绝对值的剪枝：直接比较每个通道权重的绝对值，剪除绝对值较小的通道。\n\n另外，**权重的重要性**评估方法也是包括：权重幅值（绝对值）、权重向量的 L1 范数和 L2 范数。\n\n## Reference\n\n1. [《Batch Normalization Accelerating Deep Network Training by Reducing Internal Covariate Shift》阅读笔记与实现](https://blog.csdn.net/happynear/article/details/44238541 \"《Batch Normalization Accelerating Deep Network Training by Reducing Internal Covariate Shift》阅读笔记与实现\")\n2. [详解机器学习中的梯度消失、爆炸原因及其解决方法](https://blog.csdn.net/qq_25737169/article/details/78847691 \"详解机器学习中的梯度消失、爆炸原因及其解决方法\")\n3. [N折交叉验证的作用（如何使用交叉验证）](https://zhuanlan.zhihu.com/p/113623623 \"N折交叉验证的作用（如何使用交叉验证）\")\n4. [5分钟理解Focal Loss与GHM——解决样本不平衡利器](https://zhuanlan.zhihu.com/p/80594704 \"5分钟理解Focal Loss与GHM——解决样本不平衡利器\")\n5. [深度学习CV岗位面试问题总结（目标检测篇）](https://blog.csdn.net/qq_39056987/article/details/112104199 \"深度学习CV岗位面试问题总结（目标检测篇）\")\n"
  },
  {
    "path": "3-classic_backbone/DenseNet论文解读.md",
    "content": "## 目录\n- [目录](#目录)\n- [摘要](#摘要)\n- [网络结构](#网络结构)\n- [优点](#优点)\n- [代码](#代码)\n- [问题](#问题)\n- [参考资料](#参考资料)\n\n## 摘要\n\n`ResNet` 的工作表面，只要建立前面层和后面层之间的“短路连接”（shortcut），就能有助于训练过程中梯度的反向传播，从而能训练出更“深”的 CNN 网络。`DenseNet` 网络的基本思路和 `ResNet` 一致，但是它建立的是前面所有层与后面层的**密集连接**（dense connection）。传统的 $L$ 层卷积网络有 $L$ 个连接——每一层与它的前一层和后一层相连—，而 DenseNet 网络有 $L(L+1)/2$ 个连接。\n\n在 DenseNet 中，让网络中的每一层都直接与其前面层相连，实现特征的重复利用；同时把网络的每一层设计得特别“窄”（特征图/滤波器数量少），即只学习非常少的特征图（最极端情况就是每一层只学习一个特征图），达到降低冗余性的目的。\n\n## 网络结构\n\n`DenseNet` 模型主要是由 `DenseBlock` 组成的。\n\n用公式表示，传统直连（`plain`）的网络在 $l$ 层的输出为：\n\n$$\\mathrm{x}_l = H_l(\\mathrm{\\mathrm{x}}_l-1)$$\n\n对于残差块（residual block）结构，增加了一个恒等映射（`shortcut` 连接）：\n\n$$\\mathrm{x}_l = H_l(\\mathrm{\\mathrm{x}}_l-1) + \\mathrm{x}_{l-1}$$\n\n而在密集块（`DenseBlock`）结构中，每一层都会将前面所有层 `concate` 后作为输入：\n\n$$\\mathrm{x}_l = H_l([\\mathrm{\\mathrm{x_0},\\mathrm{x_1},...,\\mathrm{x_{l-1}}]})$$\n\n$[\\mathrm{\\mathrm{x_0},\\mathrm{x_1},...,\\mathrm{x_{l-1}}]}$ 表示网络层 $0,...,l-1$ 输出特征图的拼接。这里暗示了，在 DenseBlock 中，每个网络层的特征图大小是一样的。$H_l(\\cdot)$ 是非线性转化函数（non-liear transformation），它由 BN(`Batch Normalization`)，ReLU 和 Conv 层组合而成。\n\n`DenseBlock` 的结构图如下图所示。\n\n![densenet-block结构图](../images/densenet/densenet_block_structure_diagram_1.png)\n\n在 `DenseBlock` 的设计中，作者重点提到了一个参数 $k$，被称为网络的增长率（`growth of the network`），其实是 `DenseBlock` 中任何一个 $3\\times 3$ 卷积层的滤波器个数（输出通道数）。如果每个 $H_l(\\cdot)$ 函数都输出 $k$ 个特征图，那么第 $l$ 层的输入特征图数量为 $k_0 + k\\times (l-1)$，$k_0$ 是 `DenseBlock` 的输入特征图数量（即第一个卷积层的输入通道数）。`DenseNet` 网络和其他网络最显著的区别是，$k$ 值可以变得很小，比如 $k=12$，即网络变得很“窄”，但又不影响精度。如表 4 所示。\n\n![densenet对比实验结果](../images/densenet/densenet_comparison_experimental_results_1.png)\n\n为了在 `DenseNet` 网络中，保持 `DenseBlock` 的卷积层的 feature map 大小一致，作者在两个 `DenseBlock` 中间插入 `transition` 层。其由 $2\\times 2$ average pool, stride=2，和 $1\\times 1$ conv 层组合而成，具体为 **BN + ReLU + 1x1 Conv + 2x2 AvgPooling**。`transition` 层完成降低特征图大小和降维的作用。\n> `CNN` 网络一般通过 Pooling 层或者 stride>1 的卷积层来降低特征图大小（比如 stride=2 的 3x3 卷积层），\n\n下图给出了一个 `DenseNet` 的网路结构，它共包含 `3` 个（一半用 `4` 个）`DenseBlock`，各个 `DenseBlock` 之间通过 `Transition` 连接在一起。\n\n![densenet网络结构图](../images/densenet/densenet_network_structure_diagram_1.png)\n\n和 `ResNet` 一样，`DenseNet` 也有 `bottleneck` 单元，来适应更深的 `DenseNet`。`Bottleneck` 单元是 BN-ReLU-Conv(1x1)-BN-ReLU-Conv(3x3)这样连接的结构，作者将具有 `bottleneck` 的密集单元组成的网络称为 `DenseNet-B`。\n> `Bottleneck` 译为瓶颈，一端大一端小，对应着 1x1 卷积通道数多，3x3 卷积通道数少。\n\n对于 `ImageNet` 数据集，图片输入大小为 $224\\times 224$ ，网络结构采用包含 `4` 个 `DenseBlock` 的`DenseNet-BC`，网络第一层是 `stride=2` 的 $7\\times 7$卷积层，然后是一个 `stride=2` 的 $3\\times 3$ MaxPooling 层，而后是 `DenseBlock`。`ImageNet` 数据集所采用的网络配置参数表如表 1 所示：\n\n![densenet系列网络参数表](../images/densenet/densenet_series_network_parameter_table_1.png)\n\n网络中每个阶段卷积层的 `feature map` 数量都是 `32`。\n\n## 优点\n\n1. **省参数**\n2. **省计算**\n3. **抗过拟合**\n\n> 注意，后续的 VoVNet 证明了，虽然 DenseNet 网络参数量少，但是其推理效率却不高。\n\n在 `ImageNet` 分类数据集上达到同样的准确率，`DenseNet` 所需的参数量和计算量都不到 `ResNet` 的一半。对于工业界而言，小模型（参数量少）可以显著地**节省带宽，降低存储开销**。\n> 参数量少的模型，计算量肯定也少。\n\n作者通过实验发现，**`DenseNet` 不容易过拟合，这在数据集不是很大的情况下表现尤为突出**。在一些图像分割和物体检测的任务上，基于 `DenseNet` 的模型往往可以省略在 `ImageNet` 上的预训练，直接从随机初始化的模型开始训练，最终达到相同甚至更好的效果。\n\n对于 `DenseNet` 抗过拟合的原因，作者给出的比较直观的解释是：神经网络每一层提取的特征都相当于对输入数据的一个非线性变换，而随着深度的增加，变换的复杂度也逐渐增加（更多非线性函数的复合）。相比于一般神经网络的分类器直接依赖于网络最后一层（复杂度最高）的特征，DenseNet 可以综合利用浅层复杂度低的特征，因而更容易得到一个光滑的具有更好泛化性能的决策函数。\n\n`DenseNet` 的泛化性能优于其他网络是可以从理论上证明的：去年的一篇几乎与 DenseNet 同期发布在 arXiv 上的论文（AdaNet: Adaptive Structural Learning of Artificial Neural Networks）所证明的结论（见文中 Theorem 1）表明类似于 DenseNet 的网络结构具有更小的泛化误差界。\n\n## 代码\n\n作者开源的 `DenseNet` 提高内存效率版本的代码如下。\n\n```python\n# This implementation is based on the DenseNet-BC implementation in torchvision\n# https://github.com/pytorch/vision/blob/master/torchvision/models/densenet.py\n\nimport math\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.utils.checkpoint as cp\nfrom collections import OrderedDict\n\n\ndef _bn_function_factory(norm, relu, conv):\n    def bn_function(*inputs):\n        concated_features = torch.cat(inputs, 1)\n        bottleneck_output = conv(relu(norm(concated_features)))\n        return bottleneck_output\n\n    return bn_function\n\n\nclass _DenseLayer(nn.Module):\n    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate, efficient=False):\n        super(_DenseLayer, self).__init__()\n        self.add_module('norm1', nn.BatchNorm2d(num_input_features)),\n        self.add_module('relu1', nn.ReLU(inplace=True)),\n        self.add_module('conv1', nn.Conv2d(num_input_features, bn_size * growth_rate,\n                        kernel_size=1, stride=1, bias=False)),\n        self.add_module('norm2', nn.BatchNorm2d(bn_size * growth_rate)),\n        self.add_module('relu2', nn.ReLU(inplace=True)),\n        self.add_module('conv2', nn.Conv2d(bn_size * growth_rate, growth_rate,\n                        kernel_size=3, stride=1, padding=1, bias=False)),\n        self.drop_rate = drop_rate\n        self.efficient = efficient\n\n    def forward(self, *prev_features):\n        bn_function = _bn_function_factory(self.norm1, self.relu1, self.conv1)\n        if self.efficient and any(prev_feature.requires_grad for prev_feature in prev_features):\n            bottleneck_output = cp.checkpoint(bn_function, *prev_features)\n        else:\n            bottleneck_output = bn_function(*prev_features)\n        new_features = self.conv2(self.relu2(self.norm2(bottleneck_output)))\n        if self.drop_rate > 0:  # 加入 dropout 增加模型泛化能力\n            new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)\n        return new_features\n\n\nclass _Transition(nn.Sequential):\n    def __init__(self, num_input_features, num_output_features):\n        super(_Transition, self).__init__()\n        self.add_module('norm', nn.BatchNorm2d(num_input_features))\n        self.add_module('relu', nn.ReLU(inplace=True))\n        self.add_module('conv', nn.Conv2d(num_input_features, num_output_features,\n                                          kernel_size=1, stride=1, bias=False))\n        self.add_module('pool', nn.AvgPool2d(kernel_size=2, stride=2))\n\n\nclass _DenseBlock(nn.Module):\n    def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate, efficient=False):\n        super(_DenseBlock, self).__init__()\n        for i in range(num_layers):\n            layer = _DenseLayer(\n                num_input_features + i * growth_rate,\n                growth_rate=growth_rate,\n                bn_size=bn_size,\n                drop_rate=drop_rate,\n                efficient=efficient,\n            )\n            self.add_module('denselayer%d' % (i + 1), layer)\n\n    def forward(self, init_features):\n        features = [init_features]\n        for name, layer in self.named_children():\n            new_features = layer(*features)\n            features.append(new_features)\n        return torch.cat(features, 1)\n\n\nclass DenseNet(nn.Module):\n    r\"\"\"Densenet-BC model class, based on\n    `\"Densely Connected Convolutional Networks\" <https://arxiv.org/pdf/1608.06993.pdf>`\n    Args:\n        growth_rate (int) - how many filters to add each layer (`k` in paper)\n        block_config (list of 3 or 4 ints) - how many layers in each pooling block\n        num_init_features (int) - the number of filters to learn in the first convolution layer\n        bn_size (int) - multiplicative factor for number of bottle neck layers\n            (i.e. bn_size * k features in the bottleneck layer)\n        drop_rate (float) - dropout rate after each dense layer\n        num_classes (int) - number of classification classes\n        small_inputs (bool) - set to True if images are 32x32. Otherwise assumes images are larger.\n        efficient (bool) - set to True to use checkpointing. Much more memory efficient, but slower.\n    \"\"\"\n    def __init__(self, growth_rate=12, block_config=(16, 16, 16), compression=0.5,\n                 num_init_features=24, bn_size=4, drop_rate=0,\n                 num_classes=10, small_inputs=True, efficient=False):\n\n        super(DenseNet, self).__init__()\n        assert 0 < compression <= 1, 'compression of densenet should be between 0 and 1'\n\n        # First convolution\n        if small_inputs:\n            self.features = nn.Sequential(OrderedDict([\n                ('conv0', nn.Conv2d(3, num_init_features, kernel_size=3, stride=1, padding=1, bias=False)),\n            ]))\n        else:\n            self.features = nn.Sequential(OrderedDict([\n                ('conv0', nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),\n            ]))\n            self.features.add_module('norm0', nn.BatchNorm2d(num_init_features))\n            self.features.add_module('relu0', nn.ReLU(inplace=True))\n            self.features.add_module('pool0', nn.MaxPool2d(kernel_size=3, stride=2, padding=1,\n                                                           ceil_mode=False))\n\n        # Each denseblock\n        num_features = num_init_features\n        for i, num_layers in enumerate(block_config):\n            block = _DenseBlock(\n                num_layers=num_layers,\n                num_input_features=num_features,\n                bn_size=bn_size,\n                growth_rate=growth_rate,\n                drop_rate=drop_rate,\n                efficient=efficient,\n            )\n            self.features.add_module('denseblock%d' % (i + 1), block)\n            num_features = num_features + num_layers * growth_rate\n            if i != len(block_config) - 1:\n                trans = _Transition(num_input_features=num_features,\n                                    num_output_features=int(num_features * compression))\n                self.features.add_module('transition%d' % (i + 1), trans)\n                num_features = int(num_features * compression)\n\n        # Final batch norm\n        self.features.add_module('norm_final', nn.BatchNorm2d(num_features))\n\n        # Linear layer\n        self.classifier = nn.Linear(num_features, num_classes)\n\n        # Initialization\n        for name, param in self.named_parameters():\n            if 'conv' in name and 'weight' in name:\n                n = param.size(0) * param.size(2) * param.size(3)\n                param.data.normal_().mul_(math.sqrt(2. / n))\n            elif 'norm' in name and 'weight' in name:\n                param.data.fill_(1)\n            elif 'norm' in name and 'bias' in name:\n                param.data.fill_(0)\n            elif 'classifier' in name and 'bias' in name:\n                param.data.fill_(0)\n\n    def forward(self, x):\n        features = self.features(x)\n        out = F.relu(features, inplace=True)\n        out = F.adaptive_avg_pool2d(out, (1, 1))\n        out = torch.flatten(out, 1)\n        out = self.classifier(out)\n        return out\n```\n\n## 问题\n\n1，这么多的密集连接，是不是全部都是必要的，有没有可能去掉一些也不会影响网络的性能？\n\n**作者回答**：论文里面有一个热力图（`heatmap`），直观上刻画了各个连接的强度。从图中可以观察到网络中比较靠后的层确实也会用到非常浅层的特征。\n\n注意，后续的改进版本 `VoVNet` 设计的 `OSP` 模块，去掉中间层的密集连接，只有最后一层聚合前面所有层的特征，并做了同一个实验。热力图的结果表明，去掉中间层的聚集密集连接后，最后一层的连接强度变得更好。同时，在 `CIFAR-10` 上和同 `DenseNet` 做了对比实验，`OSP` 的精度和 `DenseBlock` 相近，但是 `MAC` 减少了很多，这说明 `DenseBlock` 的这种密集连接会导致中间层的很多特征冗余的。\n\n## 参考资料\n\n- [CVPR 2017最佳论文作者解读：DenseNet 的“what”、“why”和“how”｜CVPR 2017](https://www.leiphone.com/category/ai/0MNOwwfvWiAu43WO.html)\n- https://github.com/gpleiss/efficient_densenet_pytorch"
  },
  {
    "path": "3-classic_backbone/ResNetv2论文解读.md",
    "content": "- [前言](#前言)\n- [摘要](#摘要)\n- [1、介绍](#1介绍)\n- [2、深度残差网络的分析](#2深度残差网络的分析)\n- [3、On the Importance of Identity Skip Connection](#3on-the-importance-of-identity-skip-connection)\n- [4、On the Usage of Activation Functions](#4on-the-usage-of-activation-functions)\n  - [4.1、Experiments on Activation](#41experiments-on-activation)\n  - [4.2、Analysis](#42analysis)\n- [5、Results](#5results)\n- [6、结论](#6结论)\n- [参考资料](#参考资料)\n\n## 前言\n\n> 本文的主要贡献在于通过理论分析和大量实验证明使用恒等映射（`identity mapping`）作为快捷连接（`skip connection`）对于残差块的重要性。同时，将 `BN/ReLu` 这些 `activation` 操作挪到了 `Conv`（真正的weights filter操作）之前，提出“预激活“操作，并通过与”后激活“操作做对比实验，表明对于多层网络，使用了预激活残差单元（`Pre-activation residual unit`） 的 `resnet v2` 都取得了比 `resnet v1`（或 resnet v1.5）更好的结果。\n\n## 摘要\n\n近期已经涌现出很多以深度残差网络（deep residual network）为基础的极深层的网络架构，在准确率和收敛性等方面的表现都非常引人注目。本文主要分析残差网络基本构件（`residual building block`）中的信号传播，本文发现当使用恒等映射（identity mapping）作为快捷连接（skip connection）并且将激活函数移至加法操作后面时，前向-反向信号都可以在两个 block 之间直接传播而不受到任何变换操作的影响。**同时大量实验结果证明了恒等映射的重要性**。本文根据这个发现重新设计了一种残差网络基本单元（unit），使得网络更易于训练并且泛化性能也得到提升。\n> 注意这里的实验是深层 ResNet（$\\geq$ 110 layers） 的实验，所以我觉得，应该是对于深层 ResNet，使用”预激活”残差单元（`Pre-activation residual unit`）的网络（ResNet v2）更易于训练并且精度也更高。\n\n## 1、介绍\n\n深度残差网络（`ResNets`）由残差单元（`Residual Units`）堆叠而成。每个残差单元（图1 (a)）可以表示为：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_1.png)\n\n其中，$x_l$ 和 $x_{l+1}$ 是 第 $l$ 个残差单元的输入和输出，$F$ 是残差函数。在 ResNet 中，$h(x_{l})= x_{l}$ 是恒等映射（`identity`），$f$ 是 `ReLU` 激活函数。在 `ImageNet` 数据集和 `COCO` 数据集上，超过 `1000` 层的残差网络都取得了当前最优的准确率。残差网络的核心思想是在 $h(x_{l})$ 的基础上学习附加的残差函数 $F$，其中很关键的选择就是使用恒等映射 $h(x_{l})= x_{l}$，这可以通过在网络中添加恒等快捷连接（skip connection) `shortcut` 来实现。\n\n本文中主要在于分析在深度残差网络中构建一个信息“直接”传播的路径——不只是在残差单元直接，而是在整个网络中信息可以“直接”传播。如果 $h(x_{l})$ 和 $f(y_{l})$ 都是恒等映射，那么**信号可以在单元间直接进行前向-反向传播**。实验证明基本满足上述条件的网络架构一般更容易训练。本文实验了不同形式的 $h(x_{l})$，发现使用恒等映射的网络性能最好，误差减小最快且训练损失最低。这些实验说明“干净”的信息通道有助于优化。各种不同形式的 $h(x_{l})$ 见论文中的图 1、图2 和 图4 中的灰色箭头所示。\n\n![resnet和resnetv2的残差单元结构图](../images/resnetv2/residual_unit_structure_diagram_of_resnet_and_resnetv2.png)\n\n为了构建 $f(y_l)=y_l$ 的恒等映射，本文将激活函数（`ReLU` 和 `BN`）移到权值层（`Conv`）之前，形成一种“预激活（`pre-activation`）”的方式，而不是常规的“后激活（`post-activation`）”方式，这样就设计出了一种新的残差单元（见图 `1(b)`）。基于这种新的单元我们在 `CIFAR-10/100` 数据集上使用`1001` 层残差网络进行训练，发现新的残差网络比之前（`ResNet`）的更容易训练并且泛化性能更好。同时还考察了 `200` 层新残差网络在 `ImageNet` 上的表现，原先的残差网络在这个层数之后开始出现过拟合的现象。这些结果表明**网络深度这个维度**还有很大探索空间，毕竟深度是现代神经网络成功的关键。\n\n## 2、深度残差网络的分析\n\n原先 `ResNets` 的残差单元的可以表示为：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_2.png)\n\n在 `ResNet` 中，函数 $h$ 是恒等映射，即 $h(x_{l}) = x_{l}$。公式的参数解释见下图：\n\n![残差网络表示公式的各个参数解释](../images/resnetv2/explanation_of_the_parameters_of_the_residual_network_representation_formula.png)\n\n如果函数 $f$ 也是恒等映射，即 $y_{l}\\equiv y_{l}$，公式 `(1)(2)` 可以合并为：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_3.png)\n\n那么**任意深层**的单元 $L$ 与浅层单元 $l$之间的关系为：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_4.png)\n\n公式 `(4)` 有两个特性：\n\n1. 深层单元的特征可以由浅层单元的特征和残差函数相加得到；\n2. 任意深层单元的特征都可以由起始特征 $x_0$ 与先前所有残差函数相加得到，这与普通（`plain`）网络不同，普通网络的深层特征是由一系列的矩阵向量相乘得到。**残差网络是连加，普通网络是连乘**。\n\n公式 `(4)` 也带来了良好的反向传播特性，用 $\\varepsilon $ 表示损失函数，根据反向传播的链式传导规则，反向传播公式如下：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_5.png)\n\n从公式 `(5)` 中可以看出，反向传播也是两条路径，其中之一直接将信息回传，另一条会经过所有的带权重层。另外可以注意到第二项的值在一个 `mini-batch` 中不可能一直是 `-1`，也就是说回传的梯度不会消失，不论网络中的权值的值再小都不会发生梯度消失现象。\n\n## 3、On the Importance of Identity Skip Connection\n\n考虑恒等映射的重要性。假设将恒等映射改为 $h(x_{l}) = \\lambda_{l}x_{l})$，则：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_6.png)\n\n像公式 `(4)` 一样递归的调用公式 `(3)`，得：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation7.png)\n\n其中，$\\hat{F}$ 表示将标量合并到残差函数中，与公式 `(5)` 类似，反向传播公式如下：\n\n![残差单元公式表示](../images/resnetv2/residual_unit_formula_representation_8.png)\n\n与公式 `(5)` 不同，公式 `(8)` 的第一个加法项由因子 $\\prod_{i=l}^{L-1}\\lambda_{i}$ 进行调节。对于一个极深的网络($L$ 极大)，考虑第一个连乘的项，如果所有的 $\\lambda$ 都大于 `1`，那么这一项会指数级增大；如果所有 $\\lambda$ 都小于 `1`，那么这一项会很小甚至消失，会阻断来自 `shortcut` 的反向传播信号，并迫使其流过权重层。本文通过实验证明这种方式会对模型优化造成困难。\n\n另外其他不同形式的变换映射也都会阻碍信号的有效传播，进而影响训练进程。\n\n![不同形式的shortcut](../images/resnetv2/different_forms_of_shortcuts.png)\n\n## 4、On the Usage of Activation Functions\n\n第 `3` 章考察使用不同形式映射（见图 `2`）来验证函数 $h$ 是恒等映射的重要性，这章讨论公式`(2)`中的 $f$，如果 $f$ 也是恒等映射，网络的性能会不会有所提升。通过调节激活函数 (`ReLU and/or BN`) 的位置，来使 $f$ 是恒等映射。图 `4` 展示了激活函数在不同位置的残差单元结构图去。\n> 图 `4(e)` 的”预激活“操作是本文提出的一种对于深层残差网络能够更有效训练的网络结构（`ResNet v2`）。\n\n![不同形式的relu激活](../images/resnetv2/different_forms_of_relu_activation.png)\n\n### 4.1、Experiments on Activation\n\n本章，我们使用 `ResNet-110` 和 `164` 层瓶颈结构(称为 `ResNet-164`)来进行实验。瓶颈残差单元包含一个 $1\\times 1$ 的层来降维，一个 $3\\times 3$ 的层，还有一个 $1\\times 1$ 的层来恢复维度。如 `ResNet` 论文中描述的那样，它的计算复杂度和包含两个 $3\\times 3$ 卷积层的残差单元相似。\n\n**BN after addition**\n效果比基准差，BN 层移到相加操作后面会阻碍信号传播，一个明显的现象就是训练初期误差下降缓慢。\n\n**ReLU before addition**\n这样组合的话残差函数分支的输出就一直保持非负，这会影响到模型的表示能力，而实验结果也表明这种组合比基准差。\n\n**Post-activation or pre-activation**\n原来的设计中相加操作后面还有一个 `ReLU` 激活函数，这个激活函数会影响到残差单元的两个分支，现在将它移到残差函数分支上，快捷连接分支不再受到影响。具体操作如图 `5` 所示。\n\n![Figure5](../images/resnetv2/Figure5.png)\n\n根据激活函数与相加操作的位置关系，我们称之前的组合方式为“后激活（`post-activation`）”，现在新的组合方式称之为“预激活（`pre-activation`）”。原来的设计与预激活残差单元之间的性能对比见表 `3`。预激活方式又可以分为两种：只将 `ReLU` 放在前面，或者将 `ReLU` 和 `BN`都放到前面，根据表 `2` 中的结果可以看出 `full pre-activation` 的效果要更好。\n\n![表2](../images/resnetv2/table_2.png)\n\n![表3](../images/resnetv2/table_3.png)\n\n### 4.2、Analysis\n\n使用预激活有两个方面的优点：1) $f$ 变为恒等映射，使得网络更易于优化；2)使用 `BN` 作为预激活可以加强对模型的正则化。\n\n**Ease of optimization**\n这在训练 `1001` 层残差网络时尤为明显，具体见图 1。使用原来设计的网络在起始阶段误差下降很慢，因为 $f$ 是 `ReLU` 激活函数，当信号为负时会被**截断**，使模型无法很好地逼近期望函数；而使用预激活网络中的 $f$ 是恒等映射，信号可以在不同单元直接直接传播。本文使用的 `1001`层网络优化速度很快，并且得到了最低的误差。\n\n**$f$ 为 `ReLU` 对浅层残差网络的影响并不大，如图 `6-right` 所示。本文认为是当网络经过一段时间的训练之后权值经过适当的调整，使得单元输出基本都是非负，此时 $f$ 不再对信号进行截断。但是截断现象在超过 `1000`层的网络中经常发生**。\n\n![Figure6](../images/resnetv2/Figure6.png)\n\n**Reducing overfitting**\n观察图 `6-right`，使用了预激活的网络的训练误差稍高，但却得到更低的测试误差，本文推测这是 `BN` 层的正则化效果所致。在原始残差单元中，尽管`BN` 对信号进行了标准化，但是它很快就被合并到捷径连接(`shortcut`)上，组合的信号并不是被标准化的。**这个非标准化的信号又被用作下一个权重层的输入**。与之相反，本文的预激活（`pre-activation`）版本的模型中，权重层的输入总是标准化的。\n\n## 5、Results\n\n表 `4`、表 `5` 分别展示了不同深层网络在不同数据集上的表现。使用的预激活单元的且**更深层**的残差网络（`ResNet v2`）都取得了最好的精度。\n\n![表4](../images/resnetv2/table_4.png)\n\n![表5](../images/resnetv2/table_5.png)\n\n## 6、结论\n\n**恒等映射形式的快捷连接和预激活对于信号在网络中的顺畅传播至关重要**。\n\n## 参考资料\n\n1. [[DL-架构-ResNet系] 002 ResNet-v2](https://zhuanlan.zhihu.com/p/29678910)\n2. [Identity Mappings in Deep Residual Networks（译）](https://blog.csdn.net/wspba/article/details/60750007)\n3. [Identity Mappings in Deep Residual Networks](https://arxiv.org/pdf/1603.05027.pdf)\n"
  },
  {
    "path": "3-classic_backbone/ResNet网络详解.md",
    "content": "## 摘要\n\n残差网络(`ResNet`)的提出是为了**解决深度神经网络的“退化”（优化）问题**。\n\n有[论文](https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1702.08591)指出，神经网络越来越深的时候，反传回来的梯度之间的相关性会越来越差，最后接近白噪声。即更深的卷积网络会产生梯度消失问题导致网络无法有效训练。\n\n而 `ResNet` 通过设计残差块结构，调整模型结构，让更深的模型能够有效训练更训练。目前 ResNet 被当作目标检测、语义分割等视觉算法框架的主流 backbone。\n\n## 一，残差网络介绍\n\n作者提出认为，假设一个比较浅的卷积网络已经可以达到不错的效果，那么即使新加了很多卷积层什么也不做，模型的效果也不会变差。但，之所以之前的深度网络出现退化问题，是因为让网络层什么都不做恰好是当前神经网络最难解决的问题之一！\n\n因此，作者可以提出**残差网络**的初衷，其实是让模型的内部结构至少有**恒等映射**的能力（什么都不做的能力），这样可以保证叠加更深的卷积层不会因为网络更深而产生退化问题！\n\n### 1.1，残差结构原理\n\n对于 VGG 式的卷积网络中的一个卷积 block，假设 block 的输入为 $x$ ，期望输出为 $H(x)$，block 完成非线性映射功能。\n\n那么，**如何实现恒等映射**呢？\n\n假设直连（`plain`）卷积 block 的输入为 $x$ ，block 期望输出为 $H(x)$，我们一般第一反应是直接让学习 $H(x) = x$，但是这很难！\n\n对此，作者换了个角度想问题，既然 $H(x) = x$ 很难学习到，那我就将 $H(x)$ 学习成其他的，而让恒等映射能力通过其他结构来实现，比如，直接加个 shorcut 不就完事了！这样只要直连 block 网络输出学习为 0 就行了。而让直连卷积 block 输出学习为 0 比学习成恒等映射的能力是要简单很多的！毕竟前者通过 L2 正则化就能实现了！\n\n因此，作者将网络设计为 $H(x) = F(x) + x$，即直接把恒等映射作为网络的一部分，只要 $F(x) = 0$，即实现**恒等映射**: $H(x) = x$。残差块结构（`resdiual block`）。基本残差块结构如下图所示: \n\n![image-20230217211129945](../images/resnet/image-20230217211129945.png)\n\n从图中可以看出，一个残差块有 $2$ 条路径 $F(x)$ 和 $x$，$F(x)$ 路径拟合**残差** $H(x)-x$，可称为残差路径，$x$ 路径为恒等映射（identity mapping），称其为”shortcut”。图中的 $⊕$ 为逐元素相加（`element-wise addition`），要求参与运算的 $F(x)$ 和 $x$ 的尺寸必须相同！\n> 这就把前面的问题转换成了学习一个残差函数 $F(x) = H(x) - x$。\n\n综上**总结**：可以认为 Residual Learning 的初衷（原理），其实是**让模型的内部结构至少有恒等映射的能力**。以保证在堆叠网络的过程中，网络至少不会因为继续堆叠而产生退化！\n> resnet 到底解决了什么问题以及为什么有效问题的更细节回答，可以参考这个[回答](https://www.zhihu.com/question/64494691/answer/786270699?utm_campaign=shareopn&utm_content=group3_Answer&utm_medium=social&utm_oi=815221330185170944&utm_psn=1615385485534294017&utm_source=wechat_session)。\n\n### 1.2，两种不同的残差路径\n\n在 ResNet 原论文中，残差路径的设计可以分成 $2$ 种，\n\n1. 一种没有 `bottleneck` 结构，如图3-5左所示，称之为“basic block”，由 2 个 $3\\times 3$ 卷积层构成。2 层的残差学习单元其两个输出部分必须具有相同的通道数（因为残差等于目标输出减去输入，即 $H(x) - x$，所以输入、输出通道数目需相等)。\n2. 另一种有 `bottleneck` 结构，称之为 “bottleneck block”，对于每个残差函数 $F$，使用 $3$ 层堆叠而不是 2 层，3 层分别是 $1\\times 1$，$3\\times 3$ 和 $1\\times 1$ 卷积。其中 $1\\times 1$ 卷积层负责先减小然后增加（恢复）维度，使 $3\\times 3$ 卷积层的通道数目可以降低下来，降低参数量减少算力瓶颈（这也是起名 bottleneck 的原因 ）。`50` 层以上的残差网络都使用了 bottleneck block 的残差块结构，因为其可以减少计算量和降低训练时间。\n\n![image-20230217211429369](../images/resnet/image-20230217211429369.png)\n\n> 3 层的残差学习单元是参考了 Inception Net 结构中的 `Network in Network` 方法，在中间 $3\\times 3$ 的卷积前后使用 $1\\times 1$ 卷积，实现先降低维度再提升维度，从而起到降低模型参数和计算量的作用。\n\n###  1.3，两种不同的 shortcut 路径\n\n`shortcut` 路径大致也分成 $2$ 种，一种是将输入 $x$ 直接输出，另一种则需要经过 $1\\times 1$ 卷积来升维或降采样，其是为了将 `shortcut` 输出与 `F(x)` 路径的输出保持形状一致，但是其对网络性能的提升并不明显，两种结构如图3-6所示。\n\n![image-20230217211358863](../images/resnet/image-20230217211358863.png)\n\nResidual Block（残差块）之间的衔接，在原论文中，$F(x)+x$ 是经过 ReLU 后直接作为下一个 block 的输入 $x$。\n\n## 二，ResNet18 模型结构分析\n\n残差网络中，将堆叠的几层卷积 `layer` 称为残差块（`Residual Block`），多个相似的残差块串联构成 ResNet。ResNet18 和 ResNet34 Backbone用的都是两层的残差学习单元（`basic block`），更深层的ResNet则使用的是三层的残差学习单元（`bottle block`）。\n\nResNet18 其结构如下图所示。\n\n![image-20230217212628578](../images/resnet/image-20230217212628578.png)\n\nResNet18 网络具体参数如下表所示。\n\n![image-20230217212933666](../images/resnet/image-20230217212933666.png)\n\n假设图像输入尺寸为，$1024\\times 2048$，ResNet 共有五个阶段。\n\n1. 其中第一阶段的 `conv1 layer` 为一个 $7\\times 7$ 的卷积核，`stride` 为 2，然后经过池化层处理，此时特征图的尺寸已成为输入的`1/4`，即输出尺寸为 $512\\times 1024$。\n2. 接下来是四个阶段，也就是表格中的四个 `layer`：conv2_x、conv3_x、conv4_x、conv5_x，后面三个都会降低特征图尺寸为原来的 `1/2`，特征图的下采样是通过步长为 `2` 的 conv3_1, conv4_1 和 conv5_1 执行。所以，最后输出的 feature_map 尺寸为输入尺寸降采样 $32 = 4\\times 2\\times 2\\times 2$ 倍。\n\n在工程代码中用 `make_layer` 函数产生四个 `layer` 即对应 ResNet 网络的四个阶段。根据不同层数的 ResNet(N)：\n\n1. 输入给每个 layer 的 `blocks` 是不同的，即每个阶段(`layer`)里面的残差模块数目不同（即 `layers` 列表不同)\n2. 采用的 `block` 类型（`basic` 还是 `bottleneck` 版）也不同。\n\n本文介绍的 ResNet18，使用 `basic block`，其残差模块数量（即units数量）是 [2, 2, 2, 2]，又因为每个残差模块中只包含了 2 层卷积，故残差模块总的卷积层数为 (2+2+2+2)*2=16，再加上第一层的卷积和最后一层的分类，总共是 18 层，所以命名为 ResNet18。\n\n> ResNet50 为 [3, 4, 6, 3]。\n\n## 个人思考\n\n看了后续的 `ResNeXt`、`ResNetv2`、`Densenet`、`CSPNet`、`VOVNet` 等论文，越发觉得 `ResNet` 真的算是 `Backone` 领域划时代的工作了，因为它让**深层**神经网络可以训练，基本解决了深层神经网络训练过程中的梯度消失问题，并给出了系统性的解决方案（两种残差结构），即系统性的让网络变得更“深”了。而让网络变得更“宽”的工作，至今也没有一个公认的最佳方案（`Inception`、`ResNeXt` 等后续没有广泛应用），难道是因为网络变得“宽”不如“深”更重要，亦或是我们还没有找到一个更有效的方案。\n\n## 参考资料\n\n1. [Deep Residual Learning for Image Recognition](https://arxiv.org/abs/1512.03385)\n2. https://github.com/pytorch/vision/blob/main/torchvision/models/resnet.py"
  },
  {
    "path": "3-classic_backbone/densenet.py",
    "content": "# This implementation is based on the DenseNet-BC implementation in torchvision\n# https://github.com/pytorch/vision/blob/master/torchvision/models/densenet.py\n\nimport math\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.utils.checkpoint as cp\nfrom collections import OrderedDict\n\n\ndef _bn_function_factory(norm, relu, conv):\n    def bn_function(*inputs):\n        concated_features = torch.cat(inputs, 1)\n        bottleneck_output = conv(relu(norm(concated_features)))\n        return bottleneck_output\n\n    return bn_function\n\n\nclass _DenseLayer(nn.Module):\n    def __init__(self, num_input_features, growth_rate, bn_size, drop_rate, efficient=False):\n        super(_DenseLayer, self).__init__()\n        self.add_module('norm1', nn.BatchNorm2d(num_input_features)),\n        self.add_module('relu1', nn.ReLU(inplace=True)),\n        self.add_module('conv1', nn.Conv2d(num_input_features, bn_size * growth_rate,\n                        kernel_size=1, stride=1, bias=False)),\n        self.add_module('norm2', nn.BatchNorm2d(bn_size * growth_rate)),\n        self.add_module('relu2', nn.ReLU(inplace=True)),\n        self.add_module('conv2', nn.Conv2d(bn_size * growth_rate, growth_rate,\n                        kernel_size=3, stride=1, padding=1, bias=False)),\n        self.drop_rate = drop_rate\n        self.efficient = efficient\n\n    def forward(self, *prev_features):\n        bn_function = _bn_function_factory(self.norm1, self.relu1, self.conv1)\n        if self.efficient and any(prev_feature.requires_grad for prev_feature in prev_features):\n            bottleneck_output = cp.checkpoint(bn_function, *prev_features)\n        else:\n            bottleneck_output = bn_function(*prev_features)\n        new_features = self.conv2(self.relu2(self.norm2(bottleneck_output)))\n        if self.drop_rate > 0:  # 加入 dropout 增加模型泛化能力\n            new_features = F.dropout(new_features, p=self.drop_rate, training=self.training)\n        return new_features\n\n\nclass _Transition(nn.Sequential):\n    def __init__(self, num_input_features, num_output_features):\n        super(_Transition, self).__init__()\n        self.add_module('norm', nn.BatchNorm2d(num_input_features))\n        self.add_module('relu', nn.ReLU(inplace=True))\n        self.add_module('conv', nn.Conv2d(num_input_features, num_output_features,\n                                          kernel_size=1, stride=1, bias=False))\n        self.add_module('pool', nn.AvgPool2d(kernel_size=2, stride=2))\n\n\nclass _DenseBlock(nn.Module):\n    def __init__(self, num_layers, num_input_features, bn_size, growth_rate, drop_rate, efficient=False):\n        super(_DenseBlock, self).__init__()\n        for i in range(num_layers):\n            layer = _DenseLayer(\n                num_input_features + i * growth_rate,\n                growth_rate=growth_rate,\n                bn_size=bn_size,\n                drop_rate=drop_rate,\n                efficient=efficient,\n            )\n            self.add_module('denselayer%d' % (i + 1), layer)\n\n    def forward(self, init_features):\n        features = [init_features]\n        for name, layer in self.named_children():\n            new_features = layer(*features)\n            features.append(new_features)\n        return torch.cat(features, 1)\n\n\nclass DenseNet(nn.Module):\n    r\"\"\"Densenet-BC model class, based on\n    `\"Densely Connected Convolutional Networks\" <https://arxiv.org/pdf/1608.06993.pdf>`\n    Args:\n        growth_rate (int) - how many filters to add each layer (`k` in paper)\n        block_config (list of 3 or 4 ints) - how many layers in each pooling block\n        num_init_features (int) - the number of filters to learn in the first convolution layer\n        bn_size (int) - multiplicative factor for number of bottle neck layers\n            (i.e. bn_size * k features in the bottleneck layer)\n        drop_rate (float) - dropout rate after each dense layer\n        num_classes (int) - number of classification classes\n        small_inputs (bool) - set to True if images are 32x32. Otherwise assumes images are larger.\n        efficient (bool) - set to True to use checkpointing. Much more memory efficient, but slower.\n    \"\"\"\n    def __init__(self, growth_rate=12, block_config=(16, 16, 16), compression=0.5,\n                 num_init_features=24, bn_size=4, drop_rate=0,\n                 num_classes=10, small_inputs=True, efficient=False):\n\n        super(DenseNet, self).__init__()\n        assert 0 < compression <= 1, 'compression of densenet should be between 0 and 1'\n\n        # First convolution\n        if small_inputs:\n            self.features = nn.Sequential(OrderedDict([\n                ('conv0', nn.Conv2d(3, num_init_features, kernel_size=3, stride=1, padding=1, bias=False)),\n            ]))\n        else:\n            self.features = nn.Sequential(OrderedDict([\n                ('conv0', nn.Conv2d(3, num_init_features, kernel_size=7, stride=2, padding=3, bias=False)),\n            ]))\n            self.features.add_module('norm0', nn.BatchNorm2d(num_init_features))\n            self.features.add_module('relu0', nn.ReLU(inplace=True))\n            self.features.add_module('pool0', nn.MaxPool2d(kernel_size=3, stride=2, padding=1,\n                                                           ceil_mode=False))\n\n        # Each denseblock\n        num_features = num_init_features\n        for i, num_layers in enumerate(block_config):\n            block = _DenseBlock(\n                num_layers=num_layers,\n                num_input_features=num_features,\n                bn_size=bn_size,\n                growth_rate=growth_rate,\n                drop_rate=drop_rate,\n                efficient=efficient,\n            )\n            self.features.add_module('denseblock%d' % (i + 1), block)\n            num_features = num_features + num_layers * growth_rate\n            if i != len(block_config) - 1:\n                trans = _Transition(num_input_features=num_features,\n                                    num_output_features=int(num_features * compression))\n                self.features.add_module('transition%d' % (i + 1), trans)\n                num_features = int(num_features * compression)\n\n        # Final batch norm\n        self.features.add_module('norm_final', nn.BatchNorm2d(num_features))\n\n        # Linear layer\n        self.classifier = nn.Linear(num_features, num_classes)\n\n        # Initialization\n        for name, param in self.named_parameters():\n            if 'conv' in name and 'weight' in name:\n                n = param.size(0) * param.size(2) * param.size(3)\n                param.data.normal_().mul_(math.sqrt(2. / n))\n            elif 'norm' in name and 'weight' in name:\n                param.data.fill_(1)\n            elif 'norm' in name and 'bias' in name:\n                param.data.fill_(0)\n            elif 'classifier' in name and 'bias' in name:\n                param.data.fill_(0)\n\n    def forward(self, x):\n        features = self.features(x)\n        out = F.relu(features, inplace=True)\n        out = F.adaptive_avg_pool2d(out, (1, 1))\n        out = torch.flatten(out, 1)\n        out = self.classifier(out)\n        return out"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/CSPNet论文详解.md",
    "content": "- [摘要](#摘要)\n- [1，介绍](#1介绍)\n- [2，相关工作](#2相关工作)\n- [3，改进方法](#3改进方法)\n  - [3.1，Cross Stage Partial Network](#31cross-stage-partial-network)\n  - [3.2，Exact Fusion Model](#32exact-fusion-model)\n- [4，实验](#4实验)\n  - [4.1，实验细节](#41实验细节)\n  - [4.2，消融实验](#42消融实验)\n  - [4.3，实验总结](#43实验总结)\n- [5，结论](#5结论)\n- [6，代码解读](#6代码解读)\n- [参考资料](#参考资料)\n\n> 文章同步发于 [github](https://github.com/HarleysZhang/cv_note/blob/master/7-model_compression/)、[博客园](https://www.cnblogs.com/armcvai) 和 [知乎](https://www.zhihu.com/column/c_1359601708180529152)。最新版以 `github` 为主。如果看完文章有所收获，一定要先点赞后收藏。**毕竟，赠人玫瑰，手有余香**。\n\n## 摘要\n> `CSPNet` 是作者 `Chien-Yao Wang` 于 `2019` 发表的论文 `CSPNET: A NEW BACKBONE THAT CAN ENHANCE LEARNING CAPABILITY OF CNN`。也是对 `DenseNet` 网络推理效率低的改进版本。\n\n作者认为网络推理成本过高的问题是由于**网络优化中的梯度信息重复导致的**。`CSPNet` 通过将梯度的变化从头到尾地集成到特征图中，在减少了计算量的同时可以保证准确率。`CSP`（`Cross Stage Partial Network`，简称 `CSPNet`） 方法可以**减少模型计算量和提高运行速度的同时，还不降低模型的精度**，是一种更高效的网络设计方法，同时还能和`Resnet`、`Densenet`、`Darknet` 等 `backbone` 结合在一起。\n\n## 1，介绍\n\n虽然已经出现了 `MobileNetv1/v2/v3` 和 `ShuffleNetv1/v2` 这种为移动端（`CPU`）设计的轻量级网络，但是它们所采用的基础技术-深度可分离卷积技术并不适用于 `NPU` 芯片（基于专用集成电路 (`ASIC`) 的边缘计算系统）。\n\n`CSPNet` 和不同 `backbone` 结合后的效果如下图所示。\n\n![和分类backbone结合后的效果](../../images/CSPNet/the_effect_after_combining_with_the_classification_backbone.png)\n\n和目标检测网络结合后的效果如下图所示。\n\n![和目标检测网络结合后的效果](../../images/CSPNet/the_effect_after_combining_with_the_target_detection_network.png)\n\n`CSPNet` 提出主要是为了解决三个问题：\n\n1. 增强 CNN 的学习能力，能够在轻量化的同时保持准确性。\n2. 降低计算瓶颈和 DenseNet 的梯度信息重复。\n3. 降低内存成本。\n\n## 2，相关工作\n\n**CNN 架构的设计**。\n\n**实时目标检测器**。\n\n## 3，改进方法\n\n> 原论文命名为 `Method`，但我觉得叫改进方法更能体现章节内容。\n\n### 3.1，Cross Stage Partial Network\n\n1，**DenseNet**\n\n![DenseNet的密集层权重更新公式](../../images/CSPNet/densenet_s_dense_layer_weight_update_formula.png)\n\n其中 $f$ 为权值更新函数，$g_i$ 为传播到第 $i$ 个密集层的梯度。从公式 (2) 可以发现，大量的度信息被重用来更新不同密集层的权值，这将导致无差异的密集层反复学习复制的梯度信息。\n\n2，**Cross Stage Partial DenseNet.**\n\n作者提出的 `CSPDenseNet` 的单阶段的架构如图 2(b) 所示。`CSPDenseNet` 的一个阶段是由局部密集块和局部过渡层组成（`a partial dense block and a partial transition layer`）。\n\n![DenseNet和CSPDenseNet结构图](../../images/CSPNet/densenet_and_cspdensenet_structure_diagram.png)\n\n总的来说，作者提出的 CSPDenseNet 保留了 DenseNet 重用特征特性的优点，但同时通过**截断梯度流**防止了过多的重复梯度信息。该思想通过设计一种分层的特征融合策略来实现，并应用于局部过渡层（partial transition layer）。\n\n3，**Partial Dense Block.**\n\n设计局部密集块（`partial dense block`）的目的是为了\n\n1. **增加梯度路径**:通过**分块归并**策略，可以使梯度路径的数量增加一倍。由于采用了跨阶段策略，可以减轻使用显式特征图 copy 进行拼接所带来的弊端;\n2. **每一层的平衡计算**:通常，DenseNet 基层的通道数远大于生长速率。由于在局部稠密块中，参与密集层操作的基础层通道仅占原始数据的一半，可以有效解决近一半的计算瓶颈;\n3. **减少内存流量**: 假设 `DenseNet` 中一个密集块的基本特征图大小为 $w\\times h\\times c$，增长率为 $d$，共有 $m$ 个密集块。则该密集块的 CIO为 $(c\\times m) + ((m^2+m)\\times d)/2$，而局部密集块（`partial dense block`）的 `CIO`为 $((c\\times m) + (m^2+m)\\times d)/2$。虽然 $m$ 和 $d$ 通常比 $c$ 小得多，但是一个局部密集的块最多可以节省网络一半的内存流量。\n\n4，**Partial Transition Layer**.\n\n**设计局部过渡层的目的是使梯度组合的差异最大**。局部过渡层是一种层次化的特征融合机制，它利用梯度流的聚合策略来防止不同的层学习重复的梯度信息。在这里，我们设计了两个 `CSPDenseNet` 变体来展示这种梯度流截断是如何影响网络的学习能力的。\n\n![Figure3](../../images/CSPNet/Figure3.png)\n\nTransition layer 的含义和 DenseNet 类似，是一个 1x1 的卷积层（没有再使用 `average pool`）。上图中 `transition layer` 的位置决定了梯度的结构方式，并且各有优势：\n\n- (c) 图 **Fusion First** 方式，先将两个部分进行 concatenate，然后再进行输入到Transion layer 中，采用这种做法会是的大量特梯度信息被重用，有利于网络学习；\n- (d) 图 **Fusion Last** 的方式，先将部分特征输入 Transition layer，然后再进行concatenate，这样**梯度信息将被截断**，损失了部分的梯度重用，但是由于 Transition 的输入维度比（c）图少，大大减少了计算复杂度。\n- (b) 图中的结构是论文 `CSPNet` 所采用的，其结合了 (c)、(d) 的特点，提升了学习能力的同时也提高了一些计算复杂度。 作者在论文中给出其使用不同 Partial Transition Layer 的实验结果，如下图所示。具体使用哪种结构，我们可以根据条件和使用场景进行调整。\n\n![不同Transition-layer的对比实验](../../images/CSPNet/comparative_experiments_of_different_transition_layers.png)\n\n5，**Apply CSPNet to Other Architectures.**\n\n将 `CSP` 应用到 `ResNeXt` 或者 `ResNet` 的残差单元后的结构图如下所示：\n\n![Figure5](../../images/CSPNet/Figure5.png)\n\n### 3.2，Exact Fusion Model\n\n**Aggregate Feature Pyramid.**\n\n提出了 `EFM` 结构能够更好地聚集初始特征金字塔。\n\n![FPN-GFM-EFM结构图](../../images/CSPNet/fpn_gfm_efm_structure_diagram.png)\n\n## 4，实验\n\n### 4.1，实验细节\n\n略\n\n### 4.2，消融实验\n\nEFM 在 COCO 数据集上的消融实验结果。\n\n![EFM上的消融实验结果](../../images/CSPNet/ablation_experiment_results_on_efm.png)\n\n### 4.3，实验总结\n\n从实验结果来看，分类问题中，使用 `CSPNet` 可以降低计算量，但是准确率提升很小；在目标检测问题中，使用 `CSPNet` 作为`Backbone` 带来的精度提升比较大，可以有效增强 `CNN` 的学习能力，同时也降低了计算量。\n\n## 5，结论\n\n`CSPNet` 是能够用于移动 `gpu` 或 `cpu` 的轻量级网络架构。\n\n作者认为论文最主要的贡献是**认识到冗余梯度信息问题，及其导致的低效优化和昂贵的推理计算**。同时也提出了**利用跨阶段特征融合策略和截断梯度流**来增强不同层间学习特征的可变性。\n\n此外，还提出了一种 `EFM` 结构，它结合了 `Maxout` 操作来压缩从特征金字塔生成的特征映射，这大大降低了所需的内存带宽，因此推理的效率足以与边缘计算设备兼容。\n\n实验结果表明，本文提出的基于 `EFM` 的 `CSPNet` 在移动`GPU` 和 `CPU` 的实时目标检测任务的准确性和推理率方面明显优于竞争对手。\n\n## 6，代码解读\n\n1，Partial Dense Block 的实现，代码可以直接在 Dense Block 代码的基础上稍加修改即可，代码参考 [这里]( \"这里\")。简单的 Dense Block 代码如下：\n\n```python\nclass conv2d_bn_relu(nn.Module):\n    \"\"\"\n    BN_RELU_CONV, \n    \"\"\"\n\n    def __init__(self, in_channels: object, out_channels: object, kernel_size: object, stride: object, padding: object,\n                 dilation=1, groups=1, bias=False) -> object:\n        super(BN_Conv2d, self).__init__()\n        layers = [nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, \n\t\tpadding=padding, dilation=dilation, groups=groups, bias=bias),\n\t\t\t\tnn.BatchNorm2d(in_channels),\n\t\t\t\tnn.ReLU(inplace=False)]\n\n        self.seq = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return self.seq(x)\n\nclass bn_relu_conv2d(nn.Module):\n    \"\"\"\n    BN_RELU_CONV, \n    \"\"\"\n\n    def __init__(self, in_channels: object, out_channels: object, kernel_size: object, stride: object, padding: object,\n                 dilation=1, groups=1, bias=False) -> object:\n        super(BN_Conv2d, self).__init__()\n        layers = [nn.BatchNorm2d(in_channels),\n\t\t\t\t  nn.ReLU(inplace=False),\n\t\t\t\t  nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride,\n                            padding=padding, dilation=dilation, groups=groups, bias=bias)]\n\n        self.seq = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return self.seq(x)\n\nclass DenseBlock(nn.Module):\n\n    def __init__(self, input_channels, num_layers, growth_rate):\n        super(DenseBlock, self).__init__()\n        self.num_layers = num_layers\n        self.k0 = input_channels\n        self.k = growth_rate\n        self.layers = self.__make_layers()\n\n    def __make_layers(self):\n        layer_list = []\n        for i in range(self.num_layers):\n            layer_list.append(nn.Sequential(\n                bn_relu_conv2d(self.k0 + i * self.k, 4 * self.k, 1, 1, 0),\n                bn_relu_conv2d(4 * self.k, self.k, 3, 1, 1)\n            ))\n        return layer_list\n\n    def forward(self, x):\n        feature = self.layers[0](x \"0\")\n        out = torch.cat((x, feature), 1)\n        for i in range(1, len(self.layers)):\n            feature = self.layers[i](out \"i\")\n            out = torch.cat((feature, out), 1)\n        return out\n\t\t\n# Partial Dense Block 的实现：\nclass CSP_DenseBlock(nn.Module):\n\n    def __init__(self, in_channels, num_layers, k, part_ratio=0.5):\n        super(CSP_DenseBlock, self).__init__()\n        self.part1_chnls = int(in_channels * part_ratio)\n        self.part2_chnls = in_channels - self.part1_chnls\n        self.dense = DenseBlock(self.part2_chnls, num_layers, k)\n        trans_chnls = self.part2_chnls + k * num_layers\n        self.transtion = conv2d_bn_relu(trans_chnls, trans_chnls, 1, 1, 0)\n\n    def forward(self, x):\n        part1 = x[:, :self.part1_chnls, :, :]\n        part2 = x[:, self.part1_chnls:, :, :]\n        part2 = self.dense(part2)  # 也可以是残差块单元\n        part2 = self.transtion(part2)  # Fusion lirst\n        out = torch.cat((part1, part2), 1)\n        return out\n```\n\n## 参考资料\n\n- [增强CNN学习能力的Backbone:CSPNet](https://www.cnblogs.com/pprp/p/12566116.html \"增强CNN学习能力的Backbone:CSPNet\")\n- [CSPNet——PyTorch实现CSPDenseNet和CSPResNeXt](https://zhuanlan.zhihu.com/p/263555330 \"CSPNet——PyTorch实现CSPDenseNet和CSPResNeXt\")"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/MobileNetv1论文详解.md",
    "content": "- [1、相关工作](#1相关工作)\n  - [标准卷积](#标准卷积)\n  - [分组卷积](#分组卷积)\n  - [DW 卷积](#dw-卷积)\n  - [从 Inception module 到 depthwise separable convolutions](#从-inception-module-到-depthwise-separable-convolutions)\n- [2、MobileNets 结构](#2mobilenets-结构)\n  - [2.1，深度可分离卷积](#21深度可分离卷积)\n    - [Depthwise 卷积](#depthwise-卷积)\n    - [Pointwise 卷积](#pointwise-卷积)\n  - [2.2、网络结构](#22网络结构)\n  - [2.3、宽度乘系数-更小的模型](#23宽度乘系数-更小的模型)\n  - [2.4、分辨率乘系数-减少表示](#24分辨率乘系数-减少表示)\n  - [2.5、模型结构总结](#25模型结构总结)\n- [3、实验](#3实验)\n- [4、结论](#4结论)\n- [5、基准模型代码](#5基准模型代码)\n- [个人思考](#个人思考)\n- [后续改进-MobileDets](#后续改进-mobiledets)\n- [参考资料](#参考资料)\n\n> 文章同步发于[知乎](https://zhuanlan.zhihu.com/p/359524431)。最新版以 `github` 为主。如果看完文章有所收获，一定要先点赞后收藏。**毕竟，赠人玫瑰，手有余香**。\n\n> `MobileNet` 论文的主要贡献在于提出了一种**深度可分离卷积架构（DW+PW 卷积）**，先通过理论证明这种架构比常规的卷积计算成本（`Mult-Adds`）更小，然后通过分类、检测等多种实验证明模型的有效性。\n\n## 1、相关工作\n\n### 标准卷积\n\n一个大小为 $h_1\\times w_1$ 过滤器（`2` 维卷积核），沿着 `feature map` 的左上角移动到右下角，过滤器每移动一次，将过滤器参数矩阵和对应特征图 $h_1 \\times w_1 \\times c_1$ 大小的区域内的像素点相乘后累加得到一个值，又因为 `feature map` 的数量（通道数）为 $c_1$，所以我们需要一个 `shape` 为 $ (c_1, h_1, w_1)$ 的滤波器（ `3` 维卷积核），将每个输入 featue map 对应输出像素点位置计算和的值相加，即得到输出 feature map 对应像素点的值。又因为输出 `feature map` 的数量为 $c_2$ 个，所以需要 $c_2$ 个滤波器。标准卷积抽象过程如下图所示。\n\n![标准卷积过程](../../images/mobilenetv1/standard_convolution_process.png)\n\n`2D` 卷积计算过程动态图如下，通过这张图能够更直观理解卷积核如何执行滑窗操作，又如何相加并输出 $c_2$ 个  `feature map` ，动态图来源 [这里](https://blog.csdn.net/v_july_v/article/details/51812459?utm_source=copy)。\n\n![卷积过程](../../images/mobilenetv1/conv_gif.gif)\n> Input Shape : (3, 7, 7) — Output Shape : (2, 3, 3) — K : (3, 3) — P : (1, 1) — S : (2, 2) — D : (2, 2) — G : 1\n\n上述动态图侧重于滤波器的划窗计算过程，而下图则侧重于解释标准卷积层五个概念的关系，图片来源 [这里](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.1-%E5%8D%B7%E7%A7%AF%E7%9A%84%E5%89%8D%E5%90%91%E8%AE%A1%E7%AE%97%E5%8E%9F%E7%90%86.html)。\n\n- 输入 Input Channel\n- 卷积核组 WeightsBias\n- 过滤器 Filter\n- 卷积核 kernel\n- 输出 Feature Map\n\n![三通道经过两组过滤器的卷积过程](../../images/mobilenetv1/conv3d.png)\n\n在此例中，输入是三维数据 $3\\times 32 \\times32$，经过权重参数尺寸为 $2\\times 3\\times 5\\times 5$ 的卷积层后，输出为三维 $2\\times 28\\times 28$，维数并没有变化，只是每一维内部的尺寸有了变化，一般都是要向更小的尺寸变化，以便于简化计算。\n### 分组卷积\n\n`Group Convolution` 分组卷积，最早见于 `AlexNet`。常规卷积与分组卷积的输入 feature map 与输出 feature map 的连接方式如下图所示，图片来自[CondenseNet](https://www.researchgate.net/figure/The-transformations-within-a-layer-in-DenseNets-left-and-CondenseNets-at-training-time_fig2_321325862)。\n\n![分组卷积](../../images/mobilenetv1/grouped_convolution.png)\n\n**分组卷积的定义**：**对输入 `feature map` 进行分组，然后分组分别进行卷积**。假设输入 feature map 的尺寸为 $H \\times W \\times c_{1}$，输出 feature map 数量为 $c_2$ 个，如果将输入 feature map 按通道分为 $g$ 组，则每组特征图的尺寸为 $H \\times W \\times \\frac{c_1}{g}$，**每组**对应的滤波器（卷积核）的 尺寸 为 $h_{1} \\times w_{1} \\times \\frac{c_{1}}{g}$，每组的滤波器数量为  $\\frac{c_{2}}{g}$ 个，滤波器总数依然为 $c_2$ 个，即分组卷积的卷积核 `shape` 为 $(c_2,\\frac{c_1}{g}, h_1,w_1)$。每组的滤波器只与其同组的输入 map 进行卷积，每组输出特征图尺寸为 $H \\times W \\times \\frac{c_{2}}{g}$，将 $g$ 组卷积后的结果进行拼接 (`concatenate`) 得到最终的得到最终尺寸为 $H \\times W \\times c_2$ 的输出特征图，其分组卷积过程如下图所示：\n\n![分组卷积过程2](../../images/mobilenetv1/grouped_convolution_process_2.png)\n\n**分组卷积的意义：**分组卷积是现在网络结构设计的核心，它通过通道之间的**稀疏连接**（也就是只和同一个组内的特征连接）来降低计算复杂度。一方面，它允许我们使用更多的通道数来增加网络容量进而提升准确率，但另一方面随着通道数的增多也对带来更多的 $MAC$。针对 $1 \\times 1$ 的分组卷积，$MAC$ 和 $FLOPs$ 计算如下：\n\n$$\n\\begin{align*}\n& MACC = H \\times W \\times 1 \\times 1 \\times \\frac{c_{1}}{g}\\frac{c_{2}}{g} \\times g = \\frac{hwc_{1}c_{2}}{g} \\\\\\\\\n& FLOPs = 2 \\times MACC \\\\\\\\\n& Params = g \\times \\frac{c_2}{g}\\times\\frac{c_1}{g} \\times 1\\times 1 = \\frac{c_{1}c_{2}}{g} \\\\\\\\\n& MAC = HW(c_1 + c_2) + \\frac{c_{1}c_{2}}{g} \\\\\\\\\n\\end{align*}$$\n\n从以上公式可以得出 $1\\times 1$ 分组卷积的参数量和计算量是标准卷积的 $\\frac{1}{g}$ 的结论 ，但其实对分组卷积过程进行深入理解之后也可以直接得出以上结论。\n\n**分组卷积的深入理解**：对于 $1\\times 1$ 卷积，常规卷积输出的特征图上，每一个像素点是由输入特征图的 $c_1$ 个点计算得到，而分组卷积输出的特征图上，每一个像素点是由输入特征图的 $ \\frac{c_1}{g}$个点得到（参考常规卷积计算过程）。**卷积运算过程是线性的，自然，分组卷积的参数量和计算量是标准卷积的 $\\frac{1}{g}$ 了**。\n\n当分组卷积的分组数量 = 输入 feature map 数量 = 输出 feature map 数量，即 $g=c_1=c_2$，有 $c_1$ 个滤波器，且每个滤波器尺寸为 $1 \\times K \\times K$ 时，Group Convolution 就成了 Depthwise Convolution（DW 卷积），**`DW` 卷积的卷积核权重尺寸为** $(c_{1}, 1, K, K)$。\n> 常规卷积的卷积核权重 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`）。\n\n对于分组卷积，存在**一定的限制**：**卷积层的输出通道数必须是分组数的整数倍**，即 $c_2$ 必须是 $g$ 的整数倍。这是因为分组卷积的输出特征图是将 $g$ 组卷积后的结果进行拼接得到的，所以 $c_2$ 必须是 $g$ 的整数倍。\n\n> 假设，$c_2$ 是输出通道数，$c_1$ 是输入通道数，$g$ 是分组数。\n\n### DW 卷积\n\n和标准卷积每个 Filter 内都有一个或多个卷积核 Kernal，对应每个输入通道(Input Channel)的特性不同，分组卷积和 DW 卷积的特点如下：\n- 分组卷积：分组卷积是将输入通道分成若干组，**每组的滤波器只与其同组的输入 feature map 进行卷积**，最终将每组的输出通道拼接在一起得到最终输出。\n- DW 卷积：每个 Filter 内只有一个卷积核 Kernal，对应每个输入通道(Input Channel)，即对于每个输入通道分别使用一个固定大小的卷积核进行卷积操作。\n\n分组卷积的极致是分组数数等于输入通道数，这其实就是 `DW` 卷积，可视化如下：\n\n![DW卷积](../../images/mobilenetv1/dw_conv.png)\n\n另外，对于 `pytorch` 的卷积层 api 是同时支持普通卷积、分组卷积/DW 卷积的。但值得注意的是，对于分组卷积，卷积层的输出通道数必须是分组数的整数倍，否则代码会报错！\n\n```python\nimport torch\ninput = torch.randn([20, 10, 224, 224]) # input_channels = 10\nconv3x3 = torch.nn.Conv2d(in_channels = 10, output_channels = 5, kernel_size=3, groups=5)\noutput = conv3x3(input)\nprint(conv3x3.weight.shape)\nprint(output.shape)\n```\n\n如果将 `groups=5` 改为 `groups=6`或者将 `output_channels  = 5` 改为 `6`，则会报错：\n```bash\nValueError: in_channels must be divisible by groups\nValueError: out_channels must be divisible by groups\n```\n\n### 从 Inception module 到 depthwise separable convolutions\n\n深度可分离卷积（depthwise separable convolutions）的提出最早来源于 `Xception` 论文，Xception 的论文中提到，对于卷积来说，卷积核可以看做一个三维的滤波器：通道维+空间维（Feature Map 的宽和高），常规的卷积操作其实就是实现通道相关性和空间相关性的**联合映射**。**Inception 模块的背后存在这样的一种假设：卷积层通道间的相关性和空间相关性是可以退耦合（完全可分）的，将它们分开映射，能达到更好的效果**（the fundamental hypothesis behind Inception is that cross-channel correlations and spatial correlations are sufficiently decoupled that it is preferable not to map them jointly.）。\n\n引入**深度可分离卷积的 Inception，称之为 Xception**，其作为 Inception v3 的改进版，在 ImageNet 和 JFT 数据集上有一定的性能提升，但是参数量和速度并没有太大的变化，因为 Xception 的目的也不在于模型的压缩。深度可分离卷积的 Inception 模块如图  Figure 4 所示。\n\n![Xeption](../../images/mobilenetv1/Xception.png)\n\nFigure 4 中的“极限” Inception 模块与本文的主角-深度可分离卷积模块相似，区别在于：深度可分离卷积先进行 `channel-wise` 的空间卷积，再进行 $1 \\times 1$ 的通道卷积，Figure 4 的 Inception 则相反；\n\n## 2、MobileNets 结构\n\n### 2.1，深度可分离卷积\n\n`MobileNets` 是谷歌 2017 年提出的一种高效的移动端轻量化网络，其核心是深度可分离卷积（`depthwise separable convolutions`），深度可分离卷积的核心思想是将一个完整的卷积运算分解为两步进行，分别为 Depthwise Convolution（`DW` 卷积） 与 Pointwise Convolution（`PW` 卷积）。深度可分离卷积的计算步骤和滤波器尺寸如下所示。\n\n![深度可分离卷积的计算步骤](../../images/mobilenetv1/computational_steps_of_depthwise_separable_convolution.png)\n\n![滤波器尺寸](../../images/mobilenetv1/filter_size.png)\n> 图片来源论文 [MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications](https://arxiv.org/pdf/1704.04861.pdf)。\n\n#### Depthwise 卷积\n\n> 注意本文 DW 和 PW 卷积计算量的计算与论文有所区别，本文的输出 Feature map 大小是 $D_G \\times D_G$， 论文公式是$D_F \\times D_F$。\n\n不同于常规卷积操作， Depthwise Convolution 的一个卷积核只负责一个通道，**一个通道只能被一个卷积核卷积**（不同的通道采用不同的卷积核卷积），也就是**输入通道、输出通道和分组数相同的特殊分组卷积**，因此 Depthwise（`DW`）卷积不会改变输入特征图的通道数目。深度可分离卷积的 `DW`卷积步骤如下图：\n\n![DW卷积计算步骤](../../images/mobilenetv1/dw_convolution_calculation_steps.jpg)\n\n`DW` 卷积的计算量 $MACC  = M \\times D_{G}^{2} \\times D_{K}^{2}$\n\n#### Pointwise 卷积\n\n上述 Depthwise 卷积的问题在于它让每个卷积核单独对一个通道进行计算，但是各个通道的信息没有达到交换，从而在网络后续信息流动中会损失通道之间的信息，因此论文中就加入了 Pointwise 卷积操作，来进一步融合通道之间的信息。PW 卷积是一种特殊的常规卷积，卷积核的尺寸为 $1 \\times 1$。`PW` 卷积的过程如下图：\n\n![PW卷积过程](../../images/mobilenetv1/pw_convolution_process.jpg)\n\n假设输入特征图大小为 $D_{G} \\times D_{G} \\times M$，输出特征图大小为 $D_{G} \\times D_{G} \\times N$，则滤波器尺寸为 $1 \\times 1 \\times M$，且一共有 $N$ 个滤波器。因此可计算得到 `PW` 卷积的计算量 $MACC = N \\times M \\times D_{G}^{2}$。\n\n综上：`Depthwise` 和 `Pointwise` 卷积这两部分的计算量相加为 $MACC1 =  M \\times D_{G}^{2} \\times D_{K}^{2} + N \\times M \\times D_{G}^{2}$，而标准卷积的计算量 $MACC2 = N \\times D_{G}^{2} \\times D_{K}^{2} \\times M$。所以深度可分离卷积计算量于标准卷积计算量比值的计算公式如下。\n\n$$\n\\begin{align*}\n\\frac{Depthwise \\ Separable \\ Conv}{Standard \\ Conv} &= \\frac{M \\times D_{G}^{2}(D_{K}^{2} + N)}{N \\times D_{G}^{2} \\times D_{K}^{2} \\times M} \\\\\\\\\n&= \\frac{D_{K}^{2} + N}{D_{K}^{2} \\times N} \\\\\\\\\n&= \\frac{1}{N} + \\frac{1}{D_{K}^{2}} \\\\\\\\\n\\end{align*}\n$$ \n\n可以看出 **`Depthwise + Pointwise` 卷积的计算量相较于标准卷积近乎减少了 $N$ 倍**，$N$ 为输出特征图的通道数目，同理**参数量**也会减少很多。在达到相同目的（即对相邻元素以及通道之间信息进行计算）下， 深度可分离卷积能极大减少卷积计算量，因此大量移动端网络的 `backbone` 都采用了这种卷积结构，再加上模型蒸馏，剪枝，能让移动端更高效的推理。\n\n### 2.2、网络结构\n\n$3 \\times 3$ 的深度可分离卷积 `Block` 结构如下图所示：\n\n![3x3的深度可分离卷积Block结构](../../images/mobilenetv1/3x3_depth_separable_convolution_block_structure.png)\n\n左边是带 `bn` 和 `relu` 的标准卷积层，右边是带 bn 和 relu 的深度可分离卷积层。\n$3 \\times 3$ 的深度可分离卷积 `Block` 网络的 pytorch 代码如下：\n\n```python\nclass MobilnetV1Block(nn.Module):\n    \"\"\"Depthwise conv + Pointwise conv\"\"\"\n    def __init__(self, in_channels, out_channels, stride=1):\n        super(MobilnetV1Block, self).__init__()\n        # dw conv kernel shape is (in_channels, 1, ksize, ksize)\n        self.dw = nn.Conv2d(in_channels, in_channels, kernel_size=3,stride=stride,padding=1, groups=in_channels, bias=False)\n        self.bn1 = nn.BatchNorm2d(in_channels)\n        self.pw = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n    \n    def forward(self, x):\n        out1 = F.relu(self.bn1(self.dw(x)))\n        out2 = F.relu(self.bn2(self.pw(out1)))\n        return out2\n```\n\n`MobileNet v1` 的 `pytorch` 模型导出为 `onnx` 模型后，`深度可分离卷积 block` 结构图如下图所示。\n\n![深度可分离卷积block的onnx模型结构图](../../images/mobilenetv1/onnx_model_structure_diagram_of_depth_separable_convolution_block.png)\n\n仅用 MobileNets 的 `Mult-Adds`（乘加操作）次数更少来定义高性能网络是不够的，确保这些操作能够有效实施也很重要。例如非结构化稀疏矩阵运算（unstructured sparse matrix operations）通常并不会比密集矩阵运算（dense matrix operations）快，除非是非常高的稀疏度。\n> 这句话是不是和 `shufflenet v2` 的观点一致，即不能仅仅以 FLOPs 计算量来表现网络的运行速度，除非是同一种网络架构。\n\nMobileNet 模型结构将几乎所有计算都放入密集的 1×1 卷积中（dense 1 × 1 convolutions），卷积计算可以通过高度优化的通用矩阵乘法（`GEMM`）函数来实现。 卷积通常由 `GEMM` 实现，但需要在内存中进行名为 `im2col` 的初始重新排序，然后才映射到 `GEMM`。 caffe 框架就是使用这种方法实现卷积计算。 `1×1` 卷积不需要在内存中进行重新排序，可以直接使用 `GEMM`（最优化的数值线性代数算法之一）来实现。\n\n如表 2 所示，MobileNet 模型的 `1x1` 卷积占据了 `95%` 的计算量和 `75%` 的参数，剩下的参数几乎都在全连接层中， 3x3 的 DW 卷积核常规卷积占据了很少的计算量（Mult-Adds）和参数。\n\n![表2](../../images/mobilenetv1/table_2.png)\n\n### 2.3、宽度乘系数-更小的模型\n\n尽管基本的 `MobileNet` 体系结构已经很小且网络延迟 `latency` 很低，但很多情况下特定用例或应用可能要求模型变得更小，更快。为了构建这些更小且计算成本更低的模型，我们引入了一个非常简单的参数 $\\alpha$，称为 `width 乘数`。**宽度乘数** $\\alpha$ 的作用是使每一层的网络均匀变薄。对于给定的层和宽度乘数 $\\alpha$，输入通道的数量变为 $\\alpha M$，而输出通道的数量 $N$ 变为 $\\alpha N$。具有宽度乘数 $\\alpha$ 的深度可分离卷积（其它参数和上文一致）的计算成本为：\n\n$$\\alpha M \\times D_{G}^{2} \\times D_{K}^{2} + \\alpha N \\times \\alpha M \\times D_{G}^{2}$$\n\n其中 $\\alpha \\in (0,1]$，典型值设置为 `1、0.75、0.5` 和 `0.25`。$\\alpha = 1$ 是基准 MobileNet 模型，$\\alpha < 1$ 是缩小版的 `MobileNets`。**宽度乘数的作用是将计算量和参数数量大约减少 $\\alpha^2$ 倍，从而降低了网络计算成本（ computational cost of a neural network）**。 宽度乘数可以应用于任何模型结构，以定义新的较小模型，**且具有合理的准确性、网络延迟 `latency` 和模型大小之间的权衡**。 它用于定义新的精简结构，需要从头开始进行训练模型。基准 `MobileNet` 模型的整体结构定义如表 1 所示。\n\n![表1](../../images/mobilenetv1/table_1.png)\n\n### 2.4、分辨率乘系数-减少表示\n\n减少模型计算成本的的第二个超参数（hyper-parameter）是**分辨率因子** $\\rho$，论文将其应用于输入图像，则网络的每一层 feature map 大小也要乘以 $\\rho$。实际上，论文通过设置输入分辨率来隐式设置 $\\rho$。\n将网络核心层的计算成本表示为具有宽度乘数 $\\alpha$ 和分辨率乘数 $\\rho$ 的深度可分离卷积的公式如下：\n$$\\alpha M \\times \\rho D_{G}^{2} \\times D_{K}^{2} + \\alpha N \\times \\alpha M \\times \\rho D_{G}^{2}$$\n其中 $\\rho \\in (0,1]$，通常是隐式设置的，因此网络的输入分辨率为 `224、192、160` 或 `128`。$\\rho = 1$ 时是基准(`baseline`) MobilNet，$\\rho < 1$ 时缩小版 `MobileNets`。**分辨率乘数的作用是将计算量减少 $\\rho^2$**。\n\n### 2.5、模型结构总结\n\n+ 整个网络不算平均池化层与 `softmax` 层，且将 `DW` 卷积和 `PW` 卷积计为单独的一层，则 `MobileNet` 有 `28` 层网络。+ 在整个网络结构中步长为2的卷积较有特点，卷积的同时充当下采样的功能；\n+ 第一层之后的 `26` 层都为深度可分离卷积的重复卷积操作，分为 `4` 个卷积 `stage`；\n+ 每一个卷积层（含常规卷积、深度卷积、逐点卷积）之后都紧跟着批规范化和 `ReLU` 激活函数；\n+ 最后一层全连接层不使用激活函数。\n\n## 3、实验\n\n作者分别进行了 Stanford Dogs dataset 数据集上的细粒度识别、大规模地理分类、人脸属性分类、COCO 数据集上目标检测的实验，来证明与 `Inception V3`、`GoogleNet`、`VGG16` 等 `backbone` 相比，`MobilNets` 模型可以在计算量（`Mult-Adds`）数 10 被下降的情况下，但是精度却几乎不变。\n\n## 4、结论\n\n论文提出了一种**基于深度可分离卷积的新模型架构**，称为 `MobileNets`。 在相关工作章节中，作者首先调查了一些让模型更有效的重要设计原则，然后，演示了如何通过宽度乘数和分辨率乘数来构建更小，更快的 MobileNet，通过权衡合理的精度以减少模型大小和延迟。 然后，我们将不同的 `MobileNets` 与流行的模型进行了比较，这些模型展示了出色的尺寸，速度和准确性特性。 最后，论文演示了 MobileNet 在应用于各种任务时的有效性。\n\n## 5、基准模型代码\n\n自己复现的基准 MobileNet v1 代模型 pytorch 代码如下：\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torchvision.models as models\nfrom torch import flatten\n\nclass MobilnetV1Block(nn.Module):\n    \"\"\"Depthwise conv + Pointwise conv\"\"\"\n\n    def __init__(self, in_channels, out_channels, stride=1):\n        super(MobilnetV1Block, self).__init__()\n        # dw conv kernel shape is (in_channels, 1, ksize, ksize)\n        self.dw = nn.Sequential(\n            nn.Conv2d(in_channels, 64, kernel_size=3,\n                      stride=stride, padding=1, groups=4, bias=False),\n            nn.BatchNorm2d(in_channels),\n            nn.ReLU(inplace=True)\n        )\n        # print(self.dw[0].weight.shape)  # print dw conv kernel shape\n        self.pw = nn.Sequential(\n            nn.Conv2d(in_channels, out_channels, kernel_size=1,\n                      stride=1, padding=0, bias=False),\n            nn.BatchNorm2d(out_channels),\n            nn.ReLU(inplace=True)\n        )\n\n    def forward(self, x):\n        x = self.dw(x)\n        x = self.pw(x)\n        return x\n\n\ndef convbn_relu(in_channels, out_channels, stride=2):\n    return nn.Sequential(nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride,\n                                   padding=1, bias=False),\n                         nn.BatchNorm2d(out_channels),\n                         nn.ReLU(inplace=True))\n\n\nclass MobileNetV1(nn.Module):\n    # (32, 64, 1) means MobilnetV1Block in_channnels is 32, out_channels is 64, no change in map size.\n    stage_cfg = [(32, 64, 1), \n           (64, 128, 2), (128, 128, 1),     # stage1 conv\n           (128, 256, 2), (256, 256, 1),    # stage2 conv\n           (256, 512, 2), (512, 512, 1), (512, 512, 1), (512, 512, 1), (512, 512, 1), (512, 512, 1), # stage3 conv\n           (512, 1024, 2), (1024, 1024, 1)  # stage4 conv\n    ]\n    def __init__(self, num_classes=1000):\n        super(MobileNetV1, self).__init__()\n        self.first_conv = convbn_relu(3, 32, 2)  # Input image size reduced by half\n        self.stage_layers = self._make_layers(in_channels=32)\n        self.linear = nn.Linear(1024, num_classes)  # 全连接层\n\n    def _make_layers(self, in_channels):\n        layers = []\n        for x in self.stage_cfg:\n            in_channels = x[0]\n            out_channels = x[1]\n            stride = x[2]\n            layers.append(MobilnetV1Block(in_channels, out_channels, stride))\n            in_channels = out_channels\n        return nn.Sequential(*layers)\n\n    def forward(self, x):\n        \"\"\"Feature map shape(h、w) is 224 -> 112 -> 56 -> 28 -> 14 -> 7 -> 1\"\"\"\n        x = self.first_conv(x)\n        x = self.stage_layers(x)\n\n        x = F.avg_pool2d(x, 7)  # x shape is 7*7\n        x = flatten(x, 1)       # x = x.view(x.size(0), -1)\n        x = self.linear(x)\n\n        return x\n\n\nif __name__ == \"__main__\":\n    model = MobileNetV1()\n    model.eval()                  # set the model to inference mode\n    input_data = torch.rand(1, 3, 224, 224)\n    outputs = model(input_data)\n    print(\"Model output size is\", outputs.size())\n```\n\n程序运行结果如下：\n> Model output size is torch.Size([1, 1000])\n\n## 个人思考\n\n在降低 `FLOPs` 计算量上，`MobileNet` 的网络架构设计确实很好，但是 `MobileNet` 模型在 `GPU`、`DSP` 和 `TPU` 硬件上却不一定性能好，原因是不同硬件进行运算时的行为不同，从而导致了 **`FLOPs`少不等于 `latency` 低**的问题。\n\n如果要实际解释 `TPU` 与 `DSP` 的运作原理，可能有点麻烦，可以参考下图，从结果直观地理解他们行为上的差异。考虑一个简单的 `convolution`，在 `CPU` 上 `latency` 随着 `input` 与 `output` 的`channel` 上升正相关的增加。然而在 `DSP` 上却是阶梯型，甚至在更高的 `channel` 数下存在特别低`latency` 的甜蜜点。\n\n![Convolution在CPU与DSP的行为差异](../../images/mobilenetv1/convolution_behavior_differences_between_cpu_and_dsp.png)\n\n在一定的程度上，网络越深越宽，性能越好。宽度，即通道(`channel`)的数量，网络深度，即 `layer` 的层数，如 `resnet18` 有 `18` 个卷积层。注意我们这里说的和宽度学习一类的模型没有关系，而是特指深度卷积神经网络的(**通道**)宽度。\n\n- **网络深度的意义**：`CNN` 的网络层能够对输入图像数据进行逐层抽象，比如第一层学习到了图像边缘特征，第二层学习到了简单形状特征，第三层学习到了目标形状的特征，网络深度增加也提高了模型的抽象能力。\n- **网络宽度的意义**：网络的宽度（通道数）代表了滤波器（`3` 维）的数量，滤波器越多，对目标特征的提取能力越强，即让每一层网络学习到更加丰富的特征，比如不同方向、不同频率的纹理特征等。\n\n## 后续改进-MobileDets\n\n1. `FLOPs` 低不等于 `latency` 低，尤其是在有加速功能的硬体 (`GPU`、`DSP`与 `TPU` )上不成立。\n2. `MobileNet conv block` (`depthwise separable convolution`)在有加速功能的硬件（专用硬件设计-`NPU` 芯片）上比较没有效率。\n3. 低 `channel` 数的情况下 (如网路的前几层)，在有加速功能的硬件使用**普通** `convolution` 通常会比`separable convolution` 有效率。\n4. 在大多数的硬件上，`channel` 数为 `8` 的倍数比较有利计算。\n5. `DSP` 与 `TPU` 上，一般我们需要运算为 `uint8` 形式，`quantization`（**低精度量化**）是常见的技巧。\n6. `DSP` 与 `TPU` 上，`h-Swish` 与 `squeeze-and-excitation` 效果不明显 (因为硬体设计与 `uint8` 压缩的关系)。\n7. `DSP` 与 `TPU` 上，`5x5` `convolution` 比较没效率。\n\n## 参考资料\n\n1. [Group Convolution分组卷积，以及Depthwise Convolution和Global Depthwise Convolution](https://www.cnblogs.com/shine-lee/p/10243114.html)\n2. [理解分组卷积和深度可分离卷积如何降低参数量](https://zhuanlan.zhihu.com/p/65377955)\n3. [深度可分离卷积（Xception 与 MobileNet 的点滴）](https://www.jianshu.com/p/38dc74d12fcf)\n4. [MobileNetV1代码实现](https://www.cnblogs.com/linzzz98/articles/13453810.html)\n5. [Depthwise卷积与Pointwise卷积](https://zhuanlan.zhihu.com/p/80041030)\n6. [【CNN结构设计】深入理解深度可分离卷积](https://mp.weixin.qq.com/s/IZ-nbrCL8-9w32RSYeP_bg)\n7. [FLOPs与模型推理速度](https://zhuanlan.zhihu.com/p/122943688)\n8. [MobileDets: FLOPs不等于Latency，考量不同硬体的高效架构](https://medium.com/ai-blog-tw/mobiledets-flops%E4%B8%8D%E7%AD%89%E6%96%BClatency-%E8%80%83%E9%87%8F%E4%B8%8D%E5%90%8C%E7%A1%AC%E9%AB%94%E7%9A%84%E9%AB%98%E6%95%88%E6%9E%B6%E6%A7%8B-5bfc27d4c2c8)\n9. [MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications](https://arxiv.org/pdf/1704.04861.pdf)"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/RepVGG论文详解.md",
    "content": "- [背景知识](#背景知识)\n  - [VGG 和 ResNet 回顾](#vgg-和-resnet-回顾)\n  - [MAC 计算](#mac-计算)\n  - [卷积运算与矩阵乘积](#卷积运算与矩阵乘积)\n    - [点积](#点积)\n  - [ACNet 理解](#acnet-理解)\n    - [ACBlock 的 Pytorch 代码实现](#acblock-的-pytorch-代码实现)\n- [摘要](#摘要)\n- [RepVGG 模型定义](#repvgg-模型定义)\n- [RepVGG Block 结构](#repvgg-block-结构)\n- [RepVGG Block 的结构重参数化](#repvgg-block-的结构重参数化)\n- [结论](#结论)\n- [RepVGG 的问题](#repvgg-的问题)\n- [参考资料](#参考资料)\n\n> 文章同步发于 [github](https://github.com/HarleysZhang/cv_note/blob/master/7-model_compression/)、[博客园](https://www.cnblogs.com/armcvai) 和 [知乎](https://www.zhihu.com/column/c_1359601708180529152)。最新版以 `github` 为主。如果看完文章有所收获，一定要先点赞后收藏。**毕竟，赠人玫瑰，手有余香**。\n\n> `RepVGG` 是截止到 2021.2.9 日为止最新的一个轻量级网络架构。在我的测试中，其在安霸 `CV22` 上的加速效果不如 `ShuffleNet v2`。根据作者的描述，`RepVGG` 是为 `GPU` 和专用硬件设计的高效模型，追求高速度、省内存，较少关注参数量和理论计算量。在低算力设备上，可能不如 `MobileNet` 和 `ShuffleNet` 系列适用。\n\n## 背景知识\n\n### VGG 和 ResNet 回顾\n\n1，`VGGNet` 系列网络有 `5` 个卷积阶段 ，以 `VGG16` 为例，每一个卷积阶段内有 `2~3` 个卷积层，同时每段尾部会连接一个最大池化层来缩小 `Feature map` 尺寸。每个阶段内的卷积核数量一样，越靠后的卷积核数量越多，分别是: `64-128-256-512-512`。`VGG16` 每段卷积对应的卷积层数量为 `2-2-3-3-3`，`5` 段卷积的总层数为 $2+2+3+3+3 = 13$，再加上最后的三个全连接分类层，总共是 `16` 层网络，所以命令为 `VGG16`。`5` 个卷积阶段的卷积核数量依次呈 `2` 倍递增关系: `64-128-256-512-512`。`VGG` 系列网络结构参数表如下图所示。\n\n![VGG](../../images/backbone/VGG.png)\n\n2，`ResNet18` 也拥有 `5` 个卷积阶段 ，由 1 个单独的 $7 \\times 7$ 卷积层和工程代码中 `make_layer` 函数产生的四个 `layer`（四个卷积阶段）组成，每个 `layer` 的基础残差模块（basic block）数量（即 units 数量）为 2，因为 `basic block` 中只包含了 `2` 层卷积，故所有残差模块的总层数为 $(2+2+2+2)*2=16$，再加上第一层的卷积和最后一层的分类，总共是 `18` 层，所以命名为 `ResNet18`。5 段卷积的卷积核数量也依次呈 `2` 倍递增关系: `64-64-128-256-512`。`ResNet18` 网络具体参数表如下所示。\n\n![ResNet18 网络具体参数表](../../images/RepVGG/resnet18_parameter.png)\n\n**总结：小卷积核代替大卷积核，分段卷积，卷积核数量逐段呈 `2` 倍递增，`Feature Map` 尺寸逐段呈 `1/2` 倍递减**。\n\n### MAC 计算\n\n`MAC`(memory access cost)  内存访问次数也叫内存使用量，`CNN` 网络中每个网络层 `MAC` 的计算分为读输入 `feature map` 大小、权重大小（`DDR` 读）和写输出 `feature map` 大小（`DDR` 写）三部分。\n以卷积层为例计算 `MAC`，可假设某个卷积层输入 feature map 大小是 (`Cin, Hin, Win`)，输出 feature map 大小是 (`Hout, Wout, Cout`)，卷积核是 (`Cout, Cin, K, K`)，理论 MAC（理论 MAC 一般小于 实际 MAC）计算公式如下：\n> `feature map` 大小一般表示为 （`N, C, H, W`），`MAC` 指标一般用在端侧模型推理中，端侧模型推理模式一般都是单帧图像进行推理，即 `N = 1(batch_size = 1)`，不同于模型训练时的 `batch_size` 大小一般大于 1。\n\n```python\ninput = Hin x Win x Cin  # 输入 feature map 大小\noutput = Hout x Wout x Cout  # 输出 feature map 大小\nweights = K x K x Cin x Cout + bias   # bias 是卷积层偏置\nddr_read = input +  weights\nddr_write = output\nMAC = ddr_read + ddr_write\n```\n\n### 卷积运算与矩阵乘积\n#### 点积\n- 在**数学**中，点积（英语：`Dot Product`）又称数量积或标量积（英语：Scalar Product），是一种接受两个等长的数字序列（通常是坐标向量）、返回单个数字的代数运算。\n- 在**欧几里得几何**中，两个笛卡尔坐标向量的点积常称为内积（英语：`Inner Product`），见内积空间。\n\n卷积运算，可以看作是一串内积运算，等效于矩阵相乘，因此卷积满足**交换、结合**等定律。\n\n矩阵乘积的常用性质：\n\n- **分配律**： $A(B+C) = AB+BC$\n- **结合律**： $A(BC) = (AB)C$\n- **交换律**：绝大多数情况不满足交换律，即大多数情况下 $AB \\neq  BA$。\n\n### ACNet 理解\n\n学习 [ACNet](https://arxiv.org/pdf/1908.03930.pdf) 之前，首先得理解**卷积计算的恒等式，它是“结构重参数化思想”的理论基础**，卷积计算的恒等式的示意图如下图 `2` 所示。\n> 概念结构重参数化（structural re-parameterization）指的是首先构造一系列结构（一般用于训练），并将其参数等价转换为另一组参数（一般用于推理），从而将这一系列结构等价转换为另一系列结构。\n\n![卷积计算的恒等式的示意图](../../images/RepVGG/Figure2.png)\n\n下面等式表达的意思就是对于输入特征图 $I$，先进行 $K^{(1)}$ 和 $I$ 卷积、$K^{(2)}$ 和 $I$ 卷积，再对结果进行相加，与先进行 $K^{(1)}$ 和 $K^{(2)}$ 的逐点相加后再和 $I$ 进行卷积得到的结果是一致的（可通过矩阵乘积的分配律来理解）。这是 `ACNet` 在**推理阶段不增加任何计算量的理论基础**，但同时训练阶段计算量增加，训练时间更长，需要的显存更大。\n\n$$I \\ast K^{(1)} + I \\ast K^{(2)} = I \\ast (K^{(1)} \\oplus K^{(2)})$$\n\n`ACNet` 的创新分为训练和推理阶段：\n\n+ **训练阶段**：将现有网络中的每一个 $3 \\times 3$ 卷积层换成 $3 \\times 1$ 卷积 + $1 \\times 3$卷积 + $3 \\times 3$ 卷积共三个卷积层，并将三个卷积层的计算结果进行相加得到最终卷积层的输出。因为这个过程引入的  $1 \\times 3$ 卷积和 $3 \\times 1$ 卷积是非对称的，所以将其命名为 `Asymmetric Convolution`。论文中有实验证（见论文 `Table 4`）明引入 $1 \\times 3$ 这样的水平卷积核可以提升模型对图像上下翻转的鲁棒性，竖直方向的 $3 \\times 1$ 卷积核同理。\n+ **推理阶段**：主要是对三个卷积核进行融合，这部分在实现过程中就是使用融合后的卷积核参数来初始化现有的网络。\n\n推理阶段的卷积融合操作是和 `BN` 层一起的，融合操作发生在 `BN` 之后，论文实验证明融合在 `BN` 之后效果更好些。推理阶段卷积层融合操作示意图如下所示（BN 操作省略了 $\\varepsilon$）：\n\n![推理阶段卷积层融合](../../images/RepVGG/convolutional_layer_fusion_at_the_inference_stage.png)\n\n`ACBlock` 的训练阶段权重参数转化为推理阶段的权重参数的代码如下所示。\n\n```python\ndef get_equivalent_kernel_bias(self):\n    hor_k, hor_b = self._fuse_bn_tensor(self.hor_conv, self.hor_bn)\n    ver_k, ver_b = self._fuse_bn_tensor(self.ver_conv, self.ver_bn)\n    square_k, square_b = self._fuse_bn_tensor(self.square_conv, self.square_bn)\n    self._add_to_square_kernel(square_k, hor_k)\n    self._add_to_square_kernel(square_k, ver_k)\n    return square_k, hor_b + ver_b + square_b\n\ndef _fuse_bn_tensor(self, conv, bn):\n    std = (bn.running_var + bn.eps).sqrt()\n    t = (bn.weight / std).reshape(-1, 1, 1, 1)\n    return conv.weight * t, bn.bias - bn.running_mean * bn.weight / std\n\ndef _add_to_square_kernel(self, square_kernel, asym_kernel):\n    asym_h = asym_kernel.size(2)\n    asym_w = asym_kernel.size(3)\n    square_h = square_kernel.size(2)\n    square_w = square_kernel.size(3)\n    # 水平卷积和竖直卷积分别在对应位置和 square con 的权重相加\n    square_kernel[:, :, square_h // 2 - asym_h // 2: square_h // 2 - asym_h // 2 + asym_h,\n            square_w // 2 - asym_w // 2: square_w // 2 - asym_w // 2 + asym_w] += asym_kernel\n```\n\n#### ACBlock 的 Pytorch 代码实现\n\n作者开源了[代码](https://github.com/DingXiaoH/ACNet/blob/master/custom_layers/crop_layer.py)，将原始 $3\\times 3$ 卷积替换成 $3 \\times 3 + 3 \\times 1 + 1 \\times3$ 卷积的训练阶段基础结构 `ACBlock` 代码如下：\n\n```python\nimport torch.nn as nn\n\nclass CropLayer(nn.Module):\n    \"\"\"# 去掉因为 3x3 卷积的 padding 多出来的行或者列\n    \"\"\"\n    # E.g., (-1, 0) means this layer should crop the first and last rows of the feature map. And (0, -1) crops the first and last columns\n    def __init__(self, crop_set):\n        super(CropLayer, self).__init__()\n        self.rows_to_crop = - crop_set[0]\n        self.cols_to_crop = - crop_set[1]\n        assert self.rows_to_crop >= 0\n        assert self.cols_to_crop >= 0\n\n    def forward(self, input):\n        return input[:, :, self.rows_to_crop:-self.rows_to_crop, self.cols_to_crop:-self.cols_to_crop]\n\n\nclass ACBlock(nn.Module):\n    \"\"\"# ACNet 论文提出的 3x3+1x3+3x1 卷积结构\n    \"\"\"\n    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, padding_mode='zeros', deploy=False):\n        super(ACBlock, self).__init__()\n        self.deploy = deploy\n        if deploy:\n            self.fused_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_size,kernel_size), stride=stride,\n                                      padding=padding, dilation=dilation, groups=groups, bias=True, padding_mode=padding_mode)\n        else:\n            self.square_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,\n                                         kernel_size=(kernel_size, kernel_size), stride=stride,\n                                         padding=padding, dilation=dilation, groups=groups, bias=False,\n                                         padding_mode=padding_mode)\n            self.square_bn = nn.BatchNorm2d(num_features=out_channels)\n\n            center_offset_from_origin_border = padding - kernel_size // 2\n            ver_pad_or_crop = (center_offset_from_origin_border + 1, center_offset_from_origin_border)\n            hor_pad_or_crop = (center_offset_from_origin_border, center_offset_from_origin_border + 1)\n\n            if center_offset_from_origin_border >= 0:\n                self.ver_conv_crop_layer = nn.Identity()\n                ver_conv_padding = ver_pad_or_crop\n                self.hor_conv_crop_layer = nn.Identity()\n                hor_conv_padding = hor_pad_or_crop\n            else:\n                self.ver_conv_crop_layer = CropLayer(crop_set=ver_pad_or_crop)\n                ver_conv_padding = (0, 0)\n                self.hor_conv_crop_layer = CropLayer(crop_set=hor_pad_or_crop)\n                hor_conv_padding = (0, 0)\n\n            self.ver_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(3, 1),\n                                      stride=stride,\n                                      padding=ver_conv_padding, dilation=dilation, groups=groups, bias=False,\n                                      padding_mode=padding_mode)\n\n            self.hor_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(1, 3),\n                                      stride=stride,\n                                      padding=hor_conv_padding, dilation=dilation, groups=groups, bias=False,\n                                      padding_mode=padding_mode)\n            self.ver_bn = nn.BatchNorm2d(num_features=out_channels)\n            self.hor_bn = nn.BatchNorm2d(num_features=out_channels)\n\n    def forward(self, input):\n        if self.deploy:\n            return self.fused_conv(input)\n        else:\n            square_outputs = self.square_conv(input)  # 3x3 convolution\n            square_outputs = self.square_bn(square_outputs)\n            \n            vertical_outputs = self.ver_conv_crop_layer(input)\n            vertical_outputs = self.ver_conv(vertical_outputs)  # 3x1 convolution\n            vertical_outputs = self.ver_bn(vertical_outputs)\n            \n            horizontal_outputs = self.hor_conv_crop_layer(input)\n            horizontal_outputs = self.hor_conv(horizontal_outputs)  # 1x3 convolution\n            horizontal_outputs = self.hor_bn(horizontal_outputs)\n            # 3x3 卷积、1x3 卷积、3x1 卷积输出结果直接相加(+)\n            return square_outputs + vertical_outputs + horizontal_outputs\n```\n\n## 摘要\n\n论文的主要贡献在于：\n\n+ 提出了一种简单而强有力的 CNN 架构 RepVGG，相比 `EfficientNet`、`RegNet` 等架构，`RepVGG` 具有更佳的精度-速度均衡；\n+ 提出采用重参数化技术对 `plain` 架构进行训练-推理解耦；\n+ 在图像分类、语义分割等任务上验证了 `RepVGG` 的有效性。\n\n## RepVGG 模型定义\n\n我们说的 `VGG` 式网络结构通常是指：\n\n1. 没有任何分支结构，即通常所说的 `plain` 或 `feed-forward` 架构。\n2. 仅使用 $3 \\times 3$ 类型的卷积。\n3. 仅使用 `ReLU` 作为激活函数。\n\n`VGG` 式极简网络结构的五大优势：\n\n1. **3x3 卷积非常快**。在 `GPU` 上，3x3 卷积的计算密度（理论运算量除以所用时间）可达 1x1 和 5x5 卷积的四倍。\n2. **单路架构非常快，因为并行度高**。同样的计算量，“大而整”的运算效率远超“小而碎”的运算。已有研究表明：并行度高的模型要比并行度低的模型推理速度更快。\n3. **单路架构省内存**。例如，ResNet 的 shortcut 虽然不占计算量，却增加了一倍的显存占用。\n4. **单路架构灵活性更好，容易改变各层的宽度（如剪枝）**。\n5. **RepVGG 主体部分只有一种算子**：`3x3` 卷积接 `ReLU`。在设计专用芯片时，给定芯片尺寸或造价，可以集成海量的 `3x3` 卷积-`ReLU` 计算单元来达到很高的效率，同时单路架构省内存的特性也可以帮我们少做存储单元。\n\n`RepVGG`模型的基本架构简单来说就是：将 20 多层 $3 \\times 3$ 卷积层堆叠起来，分成 5 个 stage，每个 stage 的第一层是 `stride=2` 的降采样，每个卷积层用 ReLU 作为激活函数。\n\n## RepVGG Block 结构\n\n> 模型结构的创新。\n\n相比于多分支结构（如 ResNet、Inception、DenseNet等），近年来 `Plain` 式架构模型（`VGG`）鲜有关注，主要原因是因为性能差。有研究[1]认为 ResNet 性能好的一种解释是 ResNet 的分支结构（shortcut）产生了一个大量子模型的隐式 ensemble（因为每遇到一次分支，总的路径就变成两倍），单路直连架构显然不具备这种特点。\n\n`RepVGG` 的设计是受 `ResNet` 启发得到，尽管多分支结构以对于推理不友好，但对于训练友好，本文作者提出一种新思想：**训练一个多分支模型，推理时将多分支模型等价转换为单路模型**。参考 `ResNet` 的 `identity` 与 $1 \\times 1$ 分支，设计了如下卷积模块：\n\n$$y = x + g(x) + f(x)$$\n\n其中，$x$, $g(x)$, $f(x)$ 分别对应恒等映射，$1 \\times 1$ 卷积，$3 \\times 3$ 卷积，即在训练时，为每一个 `3x3` 卷积层添加平行的 `1x1` 卷积分支和恒等映射分支，构成一个 `RepVGG Block`。这种设计是借鉴 `ResNet` 的做法，区别在于 `ResNet` 是每隔两层或三层加一分支，`RepVGG` 模型是每层都加两个分支（训练阶段）。\n\n![图2](../../images/RepVGG/figure_2.png)\n\n训练阶段，通过简单的堆叠上述 `RepVGG Block` 构建 `RepVGG` 模型；而在推理阶段，上述模块转换成 $y=h(x)$ 形式， $h(x)$ 的参数可以通过线性组合方式从训练好的模型中转换得到。\n\n## RepVGG Block 的结构重参数化\n\n训练时使用多分支卷积结构，推理时将多分支结构进行融合转换成单路 $3 \\times 3$ 卷积层，由卷积的线性（具体说就是可加性）原理，每个 RepVGG Block 的三个分支可以合并为一个 $3 \\times 3$ 卷积层（等价转换），`Figure 4` 详细描绘了这一转换过程。\n\n论文中使用 $W^{3} \\in \\mathbb{R}^{C_2 \\times C_1 \\times 3 \\times 3}$ 表示卷积核 `shape` 为 $(C_2, C_1, 3, 3)$的卷积层，$W^{1} \\in \\mathbb {R}^{C_{2} \\times C_{1}}$ 表示输入输出通道数为 $C_2$、$C_1$，卷积核为 $1 \\times 1$ 的卷积分支，采用 $\\mu^{(3)}, \\sigma^{(3)}, \\gamma^{(3)}, \\beta^{(3)}$ 表示 $3 \\times 3$ 卷积后的 `BatchNorm` 参数（平均值、标准差、比例因子、偏差），采用 $\\mu^{(1)}, \\sigma^{(1)}, \\gamma^{(1)}, \\beta^{(1)}$ 表示 $1 \\times 1$ 卷积分支后的 `BatchNorm` 参数，采用 $\\mu^{(0)}, \\sigma^{(0)}, \\gamma^{(0)}, \\beta^{(0)}$ 表示 `identity` 分支后的 `BatchNorm` 参数。假设 $M^{(1)} \\in \\mathbb{R}^{N \\times C_1 \\times H_1 \\times W_1}$, $M^{(2)} \\in \\mathbb{R}^{N \\times C_2 \\times H_2 \\times W_2}$ 分别表示输入输出矩阵，$\\ast $ 是卷积算子。当 $C_2 = C_1, H_1 = H_2, W_1 = W_2$ 时，有\n\n$$\n\\begin{split}\nM^{(2)} &= bn(M^{(1)} \\ast W^{(3)}, \\mu^{(3)}, \\sigma^{(3)}, \\gamma^{(3)}, \\beta^{(3)}) \\\\\n&+ bn(M^{(1)} \\ast W^{(1)}, \\mu^{(1)}, \\sigma^{(1)}, \\gamma^{(1)}, \\beta^{(1)}) \\\\\n&+ bn(M^{(1)}, \\mu^{(0)}, \\sigma^{(0)}, \\gamma^{(0)}, \\beta^{(0)}).\n\\end{split}\\tag{1}\n$$\n\n如果不考虑 `identity` 的分支，上述等式只有前面两部分。这里 `bn` 表示推理时 `BN` 计算函数，$1 \\leq i \\leq C_2$。`bn` 函数公式如下：\n\n$$\n\\begin{split}\nbn(M, \\mu, \\sigma, \\gamma, \\beta) = (M_{:,i,:,:} - \\mu_i) \\frac{\\gamma_i}{\\sigma_i} + \\beta.\n\\end{split}\\tag{2}\n$$\n\n首先将每一个 `BN` 及其前面的卷积层转换成一个**带有偏置向量的卷积**（吸 BN），设 $\\{w^{'}, b^{'}\\}$ 表示 **吸 BN** 之后卷积层的卷积核和偏置向量参数，卷积层和 `BN` 合并后的卷积有如下公式：\n> 推理时的卷积层和其后的 `BN` 层可以等价转换为一个带 `bias` 的卷积层（也就是通常所谓的“吸BN”），其原理参考[深度学习推理时融合BN，轻松获得约5%的提速](https://mp.weixin.qq.com/s/P94ACKuoA0YapBKlrgZl3A)。\n\n$$\n\\begin{split}\nW_{i,:,:,:}^{'} = \\frac{\\gamma_i}{\\sigma_i} W_{i,:,:,:}, \\quad b_{i}^{'} = -\\frac{\\mu_{i} \\gamma_i}{\\sigma_i} + \\beta_{i}.\n\\end{split}\\tag{3}\n$$\n\n很容易证明当 $1 \\leq i \\leq C_2$：\n\n$$\n\\begin{split}\nbn(M \\ast W,\\mu,\\sigma,\\gamma,\\beta)_{:,i,:,:} = (M \\ast W^{'})_{:,i,:,:} + b_{i}^{'}.\n\\end{split}\\tag{4}\n$$\n\n公式（4）同样适用于`identity` 分支，因为 `identity` 可以视作 `kernel` 为**单位矩阵** $1\\times 1$ 卷积。至此，三个分支的卷积层和 `BN` 合并原理和公式已经叙述完毕，可以等效于下图 Figure 4 的第二步（吸收 BN 在前）。\n\n最后一步是三个分支的的合并，也就是三个分支卷积层的融合，每个 `RepVGG Block`转换前后的输出是完全相同的，其原理参见作者的上一篇 `ACNet` 论文。通过前面的变换，可以知道 `RepVGG Block` 模块有一个 $3 \\times 3$ 卷积核，两个 $1 \\times 1$ 卷积核以及三个 `bias` 向量参数。通过简单的 `add` 方式合并三个 bias 向量可以得到融合后新卷积层的 `bias`。将 $1 \\times 1$ 卷积核用 `0` 填充 (`pad`) 成 $3 \\times 3$ 卷积核，然后和 $3 \\times 3$ 卷积核相加（`elemen twise-add`），得到融合后卷积层的 $3 \\times 3$ 卷积核。\n\n至此三个分支的卷积层合并过程讲解完毕，可以等效于 Figure 4 的第三步。\n> 卷积核细节：注意 $3 \\times 3$ 和 $1 \\times 1$ 卷积核拥有相同的 `stride`，后者的 `padding` 值比前者小于 `1`。\n\n![图4](../../images/RepVGG/figure_4.png)\n\n从上述这一转换过程中，可以理解**结构重参数化**的实质：训练时的结构对应一组参数，推理时我们想要的结构对应另一组参数；只要能把前者的参数等价转换为后者，就可以将前者的结构等价转换为后者。\n\n## 结论\n\n最后需要注明的是，`RepVGG` 是为 `GPU` 和专用硬件设计的高效模型，追求高速度、省内存，较少关注参数量和理论计算量。在低算力设备上，可能不如 MobileNet 和 ShuffleNet 系列适用。\n\n## RepVGG 的问题\n\n`RepVGG` 的推理模型**很难使用后量化方法** (`Post-Training Quantization`, `PTQ`)，比如，使用简单的 `INT8 PTQ`，ImageNet 上的 `RepVGG` 模型的准确性会降低到 `54.55%`。\n\n`RepOptimizer` 对重参数化结构量化困难的问题进行了研究，发现重参数结构的分支融合和吸 `BN` 操作，显著放大了权重参数分布的标准差。**而异常的权重分布**又会产生了过大的网络激活层数值分布，从而进一步导致该层量化损失过大，因此模型精度损失严重。\n> 参考美团技术团队文章-[通用目标检测开源框架YOLOv6在美团的量化部署实战](https://tech.meituan.com/2022/09/22/yolov6-quantization-in-meituan.html)。\n\n后续改进论文 [RepOptimizer](https://arxiv.org/pdf/2205.15242.pdf) 中，直接量化 `RepOpt-VGG` 模型，`ImageNet` 上准确率仅会下降`2.5%`。\n> `RepOptimizer`：重参数化你的优化器：VGG 型架构 + 特定的优化器 = 快速模型训练 + 强悍性能。\n## 参考资料\n\n1. [Residual Networks Behave Like Ensembles of Relatively Shallow Networks](https://arxiv.org/pdf/1605.06431.pdf)\n2. [ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks](https://arxiv.org/pdf/1908.03930.pdf)\n3. [RepVGG: Making VGG-style ConvNets Great Again](https://arxiv.org/pdf/2101.03697.pdf)\n4. https://zh.wikipedia.org/wiki/%E7%9F%A9%E9%99%A3%E4%B9%98%E6%B3%95\n5. [RepVGG：极简架构，SOTA性能，让VGG式模型再次伟大](https://zhuanlan.zhihu.com/p/344324470)\n6. [深度学习推理时融合BN，轻松获得约5%的提速](https://mp.weixin.qq.com/s/P94ACKuoA0YapBKlrgZl3A)\n7. [【CNN结构设计】无痛的涨点技巧：ACNet](https://zhuanlan.zhihu.com/p/131282789)\n8. [Markdown下LaTeX公式、编号、对齐](https://www.zybuluo.com/fyywy520/note/82980)\n9. [重参数化你的优化器：VGG 型架构 + 特定的优化器 = 快速模型训练 + 强悍性能](https://mp.weixin.qq.com/s/zdQx8MxyilB_TlXyipnYpA)\n\n"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/ShuffleNetv2论文详解.md",
    "content": "- [摘要](#摘要)\n- [1、介绍](#1介绍)\n- [2、高效网络设计的实用指导思想](#2高效网络设计的实用指导思想)\n  - [G1-同样大小的通道数可以最小化 1x1 卷积 MAC](#g1-同样大小的通道数可以最小化-1x1-卷积-mac)\n  - [G2-分组数太多的卷积会增加 MAC](#g2-分组数太多的卷积会增加-mac)\n  - [G3-网络碎片化会降低并行度](#g3-网络碎片化会降低并行度)\n  - [G4-逐元素的操作不可忽视](#g4-逐元素的操作不可忽视)\n- [3、ShuffleNet V2：一个高效的架构](#3shufflenet-v2一个高效的架构)\n- [4、实验](#4实验)\n- [5、结论](#5结论)\n- [6，个人思考](#6个人思考)\n- [参考资料](#参考资料)\n\n> 近期在研究轻量级 backbone 网络，工业界应用较多的模型有 `MobileNet V2`、`ShuffleNet V2`、`RepVGG` 等，本篇博客是对 `ShuffleNet v2` 论文的个人理解分析。本文的参考资料是自己对网络上资料进行查找和筛选出来的，质量相对较高、且对本文有参考意义的文章。`ShuffleNet v2` 论文最大的贡献在于看到了 GPU 访存带宽（内存访问代价 MAC）对于模型推理时间的影响，而不仅仅是模型复杂度：`FLOPs` 和参数量 `Params` 对于推理时间的影响，并由此提出了 `4` 个轻量级网络设计的原则和一个新颖的 卷积 block 架构-ShuffleNet v2。\n\n## 摘要\n\n当前，神经网络结构的设计基本由间接的计算复杂度主导，例如 `FLOPs`，但是直接的度量如速度，还取决于其他因素，例如内存的获取损耗和平台特性。因此，我们将使用直接的标准衡量，而不仅仅是 `FLOPs`。因此本文建议直接在目标平台上用直接度量进行测试。基于一系列控制条件实验，作者提出了**设计高效网络结构的一些实用指导思想**，并据此提出了一个称之为 `ShuffleNet V2` 的新结构。综合的对比实验证明了作者的模型在速度和准确性上取得了最佳的平衡（`state-of-the-art`）。\n\n## 1、介绍\n\n为了衡量计算复杂度，一个广泛采用的度量方式是浮点运算的次数 `FLOPs`，但是，它是一个间接的度量，是对我们真正关心的直接度量比如速度或者时延的一种近似估计。在以前的工作中，这种不一致已经被学者们所发现，比如 MobileNet v2 要比 NASNET-A 快很多，但是它们两者具有差不多的 FLOPs。\n\n![图1](../../images/shufflenetv2/figure_1.png)\n\n图 1 中在 `GPU` 和 `ARM` 两个平台上，具有相同 `FLOPs` 的模型运行速度也会相差很多。因此只用 FLOPs 来衡量计算复杂度是不充分的，也会导致得不到最优的网络设计。\n\n导致这种不一致的主要有两个原因：一是影响速度的几个重要因素只通过 FLOPs 是考虑不到的，比如 `MAC（Memory Access Cost）`和并行度；二是具有相同 FLOPs 的模型在不同的平台上可能运行速度不一样。\n因此，作者提出了设计有效网络结构的两个原则。一是用直接度量来衡量模型的性能，二是直接在目标平台上进行测试。\n\n## 2、高效网络设计的实用指导思想\n\n首先，作者分析了两个经典结构 ShuffleNet v1 和 MobileNet v2 的运行时间。ARM 和 GPU 平台的具体运行环境如下图所示。\n\n![ARM和GPU的具体运行环境](../../images/shufflenetv2/specific_operating_environment_of_arm_and_gpu.png)\n\n![图2](../../images/shufflenetv2/figure_2.jpg)\n\n从图 2 可以看出，虽然以 `FLOPs` 度量的卷积占据了大部分的时间，但其余操作也消耗了很多运行时间，比如数据输入输出、通道打乱和逐元素的一些操作（张量相加、激活函数）。因此，FLOPs 不是实际运行时间的一个准确估计。\n\n1. **G1**：同样大小的通道数可以最小化 `MAC`。\n2. **G2**：太多的分组卷积会增加 MAC。\n3. **G3**：网络碎片化会减少并行度。\n4. **G4**：逐元素的操作不可忽视。\n\n### G1-同样大小的通道数可以最小化 1x1 卷积 MAC\n\n现代的网络如 `Xception [12], MobileNet [13], MobileNet V2 [14], ShuffleNet [15]` 都采用了深度可分离卷积，它的点卷积（即 $1\\times 1$ 卷积）占据了大部分的计算复杂度（ShuffleNet 有分析）。假设输入特征图大小为 $h*w*c_1$，那么卷积核 shape 为 $(c_2, c_1, 1, 1)$，输出特征图长宽不变，那么 $1 \\times 1$ 卷积的 `FLOPs` 为 $B = hwc_{1}c_{2}$。\n> 论文中 `FLOPs` 的计算是把乘加当作一次浮点运算的，所以其实等效于我们通常理解的 `MACs` 计算公式。\n\n简单起见，我们假设计算设备的缓冲足够大能够存放下整个特征图和参数。那么 $1 \\times 1$ 卷积层的内存访问代价（内存访问次数）为 $\\text{MAC} = hwc_1 + hwc_2 + c_{1}c_{2} = hw(c_{1} + c_{2}) + c_{1}c_{2}$，等式的三项分别代表输入特征图、输出特征图和权重参数的代价。由均值不等式，我们有：\n\n$$\n\\begin{split}\n\\text{MAC} &= hw(c_{1} + c{2}) + c_{1}c_{2} \\\\\\\\\n&= \\sqrt{(hw)^{2}(c_{1} + c_{2})^{2}} + \\frac{B}{hw} \\\\\\\\\n&\\geq \\sqrt{(hw)^{2}(4c_{1}c_{2})}+ \\frac{B}{hw} \\\\\\\\\n&\\geq 2\\sqrt{hwB} + \\frac{B}{hw} \\\\\\\\\n\\end{split}$$\n\n由均值不等式，可知当 $c_1 = c_2$ 时，$(c_{1} + c_{2})^{2} = 4c_{1}c_{2}$，即式子 $(c_{1} + c_{2})^{2}$ 取下限。即当且仅当 $c_{1}=c_{2}$ （$1 \\times 1$ **卷积输入输出通道数相等**）时，`MAC` 取得由 FLOPs 给出的最小值。\n\n但是这个结论只是理论上成立的，实际中缓存容量可能不够大，缓存策略也因平台各异。所以我们进一步设计了一个对比试验来验证，实验的基准的网络由 10 个卷积 `block` 组成，每个块有两层卷积，第一个卷积层输入通道数为 $c_{1}$ 输出通道数为$c_{2}$，第二层与第一层相反，然后固定总的 FLOPs 调整$c_{1}:c_{2}$的值测试实际的运行速度，结果如表 1 所示：\n\n![表1](../../images/shufflenetv2/table_1.png)\n> 这个实验设计的卷积 block 很有意思，首先就是它不能层数太少。\n\n可以看到，当比值接近 `1:1` 的时候，网络的 `MAC` 更小，测试速度也最快。这里可以结合 Roofline 模型分析，$1\\times 1$ 卷积本质是处于内存受限的情况，因此只有减少 `MAC` 才能从源头上减少网络层的运行时间。\n\n下述是我参考论文作者实验设计，构建的基准测试代码:\n\n```python\nimport torch\nimport torch.nn as nn\nimport time\n\n# 生成随机数据进行测试\ndef benchmark(model, input_tensor, batch_size, device, num_iterations=100):\n    model.to(device)\n    input_tensor = input_tensor.to(device)\n    \n    # 确保模型处于评估模式\n    model.eval()\n    \n    # 预热 GPU\n    for _ in range(10):\n        with torch.no_grad():\n            model(input_tensor)\n    \n    start_time = time.time()\n    for _ in range(num_iterations):\n        with torch.no_grad():\n            output = model(input_tensor)\n    \n    elapsed_time = time.time() - start_time\n    images_per_sec = batch_size * num_iterations / elapsed_time\n    return images_per_sec\n\n# 定义卷积块\nclass ConvBlock(nn.Module):\n    def __init__(self, c1, c2):\n        super(ConvBlock, self).__init__()\n        self.conv1 = nn.Conv2d(c1, c2, kernel_size=1, stride=1, padding=1)\n        self.conv2 = nn.Conv2d(c2, c1, kernel_size=1, stride=1, padding=1)\n        self.relu = nn.ReLU(inplace=True)\n\n    def forward(self, x):\n        x = self.relu(self.conv1(x))  # 第一层卷积\n        x = self.relu(self.conv2(x))  # 第二层卷积\n        return x\n\n# 构建由 10 个卷积块组成的网络\nclass ConvNet(nn.Module):\n    def __init__(self, c1, c2, num_blocks=10):\n        super(ConvNet, self).__init__()\n        self.blocks = nn.Sequential(*[ConvBlock(c1, c2) for _ in range(num_blocks)])\n\n    def forward(self, x):\n        return self.blocks(x)\n\n# 配置参数\ninput_size = (56, 56)  # 输入图片的大小\nbatch_sizes = [1, 2, 4]  # 不同的 batch 大小\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'  # 使用 GPU 还是 CPU\n\n# 四种通道配置 (c1:c2) 比例\nchannel_configs = {\n    \"1:1\": (128, 128),\n    \"1:2\": (90, 180),\n    \"1:6\": (52, 312),\n    \"1:12\": (36, 432)\n}\n\n# 逐一测试每种通道配置和不同 batch 大小的性能\nfor ratio, (c1, c2) in channel_configs.items():\n    print(f\"Testing ratio {ratio} with channels ({c1}, {c2})\")\n    model = ConvNet(c1, c2).to(device)\n    \n    for batch_size in batch_sizes:\n        input_tensor = torch.randn(batch_size, c1, input_size[0], input_size[1])\n        images_per_sec = benchmark(model, input_tensor, batch_size, device=device)\n        print(f\"Batch size {batch_size}, Images/sec: {images_per_sec:.2f}\")\n```\n\n代码在 `macbook m3pro`（arm cpu）机器上运行后结果如下所示，另外，对于 $3\\times 3$ 卷积和 bs>1 输入的情况，即计算密集型，输入输出通道数相等并不能减少模型运行时间。\n\n![g1 基准自行测试实验结果](../../images/shufflenetv2/g1test.png)\n![g 基准自行测试实验结果2](../../images/shufflenetv2/g1test2.png)\n\n### G2-分组数太多的卷积会增加 MAC\n\n分组卷积是现在轻量级网络结构（`ShuffleNet/MobileNet/Xception/ResNeXt`）设计的核心，它通过通道之间的**稀疏连接**（也就是只和同一个组内的特征连接）来降低计算复杂度。一方面，它允许我们使用更多的通道数来增加网络容量进而提升准确率，但另一方面随着通道数的增多也对带来更多的 `MAC`。\n\n针对 $1 \\times 1$ 的分组卷积，我们有：\n> 分组卷积 `FLOPs` 的计算公式，我写的 [MobileNet v1 论文详解](https://zhuanlan.zhihu.com/p/359497513) 有给出推导。\n\n$$\n\\begin{split}\nB = h \\ast w \\ast 1 \\ast 1 \\ast \\frac{c_1}{g} \\ast \\frac{c_2}{g} \\ast g = \\frac{hwc_{1}c_{2}}{g}\n\\end{split}\n$$\n\n$$ \n\\begin{split}\n\\text{MAC} = hw(c_{1} + c_{2}) + \\frac{c_{1}c_{2}}{g} = hwc_{1} + \\frac{Bg}{c_1}+\\frac{B}{hw}\\end{split}\n$$\n\n> 固定 $\\frac{c_2}{g}$ 的比值，又因为输入特征图 $c_{1} \\times h \\times w$ 固定，从而也就固定了计算代价 $B$，所以可得 上式中 $MAC$ 与 $g$ 成正比的关系。\n\n其中 $B$ 是卷积层的浮点运算次数（`FLOPs`），$g$ 是分组卷积的组数，可以看到，如果给定输入特征图尺寸（`shape`）$c_{1} \\times h \\times w$ 和计算代价 $B$，则 $MAC$ 与组数 $g$ 成正比。本文通过叠加 10 个分组点卷积层设计了实验，在保证计算代价（`FLOPs`）相同的情况下采用不同的分组组数测试模型的运行时间，结果如下表 2 所示。\n\n![表2](../../images/shufflenetv2/table_2.png)\n\n很明显使用分组数多的网络速度更慢，比如 分为 `8` 个组要比 `1` 个组慢得多，主要原因在于 `MAC` 的增加 。因此，本文建议要根据硬件平台和目标任务谨慎地选择分组卷积的组数，不能简单地因为可以提升准确率就选择很大的组数，而忽视了内存访问代价（`MAC`）的增加。\n\n### G3-网络碎片化会降低并行度\n\n在 `GoogLeNet` 系列和自动搜索得到的网络架构中，每个网络的 `block` 都采用多分支（`multi-path`）结构，在这种结构中多采用小的算子（fragmented operators 支路算子/碎片算子）而不是大的算子，`block` 中的每一个卷积或者池化操作称之为一个 fragmented operator。如 NASNET-A[9]网络的碎片算子的数量（即一个 building block 的单个卷积或池化操作的总数）为 `13`。相反，在 `ResNet`[4] 这样的标准网络中，碎片算子的数量为 2 或者 3。\n> Residual Block 有两种，`basic block` 和 `bottleneck block` 的残差结构。`fragment`，翻译过来就是分裂的意思，可以简单理解为网络的单元或者支路数量。\n\n尽管过去的论文已经表明，这种 fragmented structure（碎片化/支路结构）能够提升模型的准确性，但是其会降低效率，因为这种结构 `GPU` 对并行性强的设备不友好。而且它还引入了额外的开销，如内核启动和同步。\n> kernel launching and synchronization. `synchronization`：同步支路结构分支之间的同步。network fragmentation 我翻译为网络碎片化。\n\n为了量化网络碎片化（network fragmentation）如何影响效率，我们评估了一系列具有不同碎片化程度(degree of fragmentation)的网络块（network blocks）。具体来说，对比实验实验的每个构建块由 1 到 4 个 顺序或并行结构的1x1 卷积层组成。The block structure 如附录图1所示。\n\n![附录图1](../../images/shufflenetv2/appendix_figure_1.png)\n\n每个 `block` 重复堆叠 10 次。表 3 的结果表明，碎片化会降低 GPU 的速度，例如 4-fragment 比 1-fragment 结构慢约 3 倍。但是在 `ARM` 上，`fragmentation` 对速度的影响速会比 `GPU` 相对较小些。\n\n![表3](../../images/shufflenetv2/table_3.png)\n\n### G4-逐元素的操作不可忽视\n\n如图 2 所示，例如 `MobileNet v2` 和 `ShuffleNet v1` 这样的轻量级模型中，按元素操作（`element-wise`）会占用大量时间的，尤其是在 `GPU` 平台上。\n\n在我们的论文中，逐元素算子包括 `ReLU`、`AddTensor`、`AddBias` 等，它们的 `FLOPs` 相对较小，但是 `MAC` 较大。特别地，我们把 `depthwise convolution` 当作一个 逐元素算子（element-wise operator），因为它的 `MAC/FLOPs` 的比值也较高。\n> `shortcut` 操作并不能当作 element-wise 算子。\n\n论文使用 `ResNet` 的 \"bottleneck\" 单元进行实验，其是由 $1 \\times 1$ 卷积、然后是$3 \\times 3$ 卷积，最后又是 $1 \\times 1$ 卷积组成，并带有 `ReLU` 和 `shortcut` 连接，其结构图如下图所示。在论文的实验中，删除 `ReLU` 和 `shortcut` 操作，表 4 报告了不同变体 \"bottleneck\" 的运行时间。我们观察到，在删除  ReLU 和 shortcut 后，在 GPU 和 CPU 平台都取得了 `20%` 的加速。\n\n![bottleneck](../../images/shufflenetv2/bottleneck.png)\n\n![表4](../../images/shufflenetv2/table_4.png)\n\n**结论和讨论**。根据上诉 4 个指导原则和经验研究，我们得出高效的网络结构应该满足：\n1. 使用平衡的卷积，也就是通道数一样；\n2. 合理使用分组卷积；\n3. 减少碎片度；\n4. 减少逐元素操作。\n\n**以上 4 个理想的属性的发挥效果取决于平台特性**（例如内存操作和代码优化），这超出了论文理论上的范围，但在实际网络设计中我们应尽量遵守这些原则。\n之前轻量级神经网络体系结构的进展主要是基于 `FLOPs` 的度量标准，并没有考虑上述 4 个属性。比如 `ShuffleNet v1` 严重依赖分组卷积，这违反了 `G2`；`MobileNet v2` 利用了反转瓶颈结构，这违反了 `G1`，而且在通道数较多的扩展层使用 `ReLU` 和深度卷积，违反了 `G4`，`NAS` 网络生成的结构碎片化很严重，这违反了 `G3`。\n\n## 3、ShuffleNet V2：一个高效的架构\n\n**重新审查 ShuffleNet v1**。`ShuffleNet`  是一个 state-of-the-art 网络，被广泛应用于低端设备中（如手机）。它启发了我们论文中的工作，因此，它首先被审查和分析。\n\n根据 `ShuffleNet v1`，轻量级神经网络的主要挑战在于，在给定预算（`FLOPs`）的情况下，特征图的通道数也是受限制的。为了在不显著增加 `FLOPs` 计算量的情况下提高通道数，`ShuffleNet v1` 论文采用了两种技术：**逐点组卷积和类瓶颈结构**（pointwise group convolutions and bottleneck-like structures.）；然后引入“channel shuﬄe” 操作，令不同组的通道之间能够进行信息交流，提高精度。其构建模块如图 3(a)(b) 所示。\n\n从本文 Section 2 的讨论，可以知道**逐点组卷积和瓶颈结构**都增加了 `MAC`( `G2` 和 `G1` )。这个成本不可忽视，特别是对于轻量级模型。另外，使用太多分组也违背了 `G3`。`shortcut connection` 中的**逐元素加法**（element-wise \"Add\"）操作也不可取 (`G4`)。因此，为了实现较高的**模型容量和效率**，关键问题是**如何保持大量且同样宽的通道，同时没有密集卷积也没有太多的分组**。\n> **如何保持大量且同样宽的通道，同时没有密集卷积也没有太多的分组**，这句话比较难理解。我的理解：1，卷积 `block` 里面的卷积层通道多且同样宽的通道的，意味着两个连接的卷积层的通道数要多且相等。2，这里密集卷积是指 $1\\times 1$ 卷积。3，使用分组卷积时，分组数 `group` 不宜过多，那就意味着 `DW` 卷积的输入通道数要较小。\n\n**ShuffleNet v2 的通道拆分**。在 ShuffleNet v1 `block`的基础上，**ShuffleNet v2 block 引入通道分割（`Channel Split`）这个简单的算子**来实现上述目的，如图 3(c) 所示。在每个单元 (block) 的开始，我们将输入特征图的 $c$ 个通道切分成 (`split`) 两个分支 (`branches`)：$c-c^{'}$ 个通道和 $c^{'}$ 个通道。根据 **G3** 网络碎片尽可能少，其中一个分支保持不变（shortcut connection），另外一个分支包含三个通道数一样的卷积来满足 **G1**。和 v1 不同，v2 block 的两个 $1 \\times 1$ 卷积不再使用分组卷积，一部分原因是为了满足 **G2**，另外一部分原因是一开始的通道切分 （`split`）操作已经完成了分组效果。\n\n最后，对两个分支的结果进行拼接（`concatnate`），这样对于卷积 `block` 来说，输入输出通道数是一样的，符合 **G1** 原则。和 `ShuffleNet v1` 一样**都使用通道打乱（`channel shuffle`）操作来保证两个分支的信息进行交互**。\n> ResNet 的 basic block 和 bottleneck block 也是这样设计的，符合 **G1** 原则。\n\n![图3](../../images/shufflenetv2/figure_3.png)\n\n通道打乱之后的输出，就是下一个单元的输入。ShuffleNet v1 的 “Add” 操作不再使用，逐元素操作算子如：`ReLU` 和 `DW 卷积` 只存在于在右边的分支。与此同时，我们将三个连续的逐元素操作算子：拼接（`“Concat”`）、通道打乱（`“Channel Shuffle”`）和通道拆分（`“Channel Split”`）**合并成一个逐元素算子**。根据 **G4**原则，这些改变是有利的。\n\n针对需要进行空间下采样的 `block`，卷积单元（`block`）进行了修改，通道切分算子被移除，然后 `block` 的输出通道数变为两倍，详细信息如图 3(d) 所示。\n\n图 3(c)(d) 显示的卷积 `block`叠加起来即组成了最后的 `ShuffleNet v2` 模型，简单起见，设置 $c^{'} = c/2$，这样堆叠后的网络是类似 ShuffleNet v1 模型的，网络结构详细信息如表 5 所示。v1 和 v2 block 的区别在于， v2 在全局平均池化层（global averaged pooling）之前添加了一个 $1 \\times 1$ 卷积来混合特征（mix up features），而 v1 没有。和 v1 一样，**v2 的 `block` 的通道数是按照 `0.5x 1x` 等比例进行缩放，以生成不同复杂度的 ShuffleNet v2 网络**，并标记为 ShuffleNet v2 0.5×、ShuffleNet v2 1× 等模型。\n> 注意：表 5 的通道数设计是为了控制 `FLOPs`，需要调整通道数将 `FLOPs` 与之前工作对齐从而使得对比实验公平，没有使用 `2^n` 通道数是因为其与精度无关。\n\n![表5](../../images/shufflenetv2/table_5.png)\n\n> 根据前文的分析，我们可以得出此架构遵循所有原则，因此非常高效。\n\n**网络精度的分析**。`ShuffleNet v2` 不仅高效而且精度也高。有两个主要理由：一是高效的卷积 `block` 结构允许我们使用更多的特征通道数，网络容量较大。二是当 $c^{'} = c/2$时，一半的特征图直接经过当前卷积 block 并进入下一个卷积 block，这类似于 `DenseNet` 和 `CondenseNet` 的特征重复利用。\n> DenseNet 是一种具有密集连接的卷积神经网络。在该网络中，任何两层之间都有直接的连接，也就是说，网络每一层的输入都是前面所有层输出的并集，而该层所学习的特征图也会被直接传给其后面所有层作为输入。\n\n在 DenseNet 论文中，作者通过画不同权重的 `L1` 范数值来分析特征重复利用的模式，如图 4(a)所示。可以看到，**相邻层之间的关联性是远远大于其它层的，这也就是说所有层之间的密集连接可能是多余的**，最近的论文 CondenseNet 也支持这个观点。\n\n![图4](../../images/shufflenetv2/figure_4.png)\n\n在 ShuffleNet V2 中，很容易证明，第 $i$ 层和第 $i+j$ 层之间直接相连的特征图通道数为 $r^{j}c$，其中 $r=(1−c^{'})/c$。换句话说，两个 blocks 之间特征复用的数量是随着两个块之间的距离变大而呈指数级衰减的。相距远的 blocks，特征重用会变得很微弱。\n> 图 4 的两个 blocks 之间关联性的理解有些难。\n\n因此，和 DenseNet 一样，Shufflenet v2 的结构通过设计实现了特征重用模式，从而得到高精度，并具有更高的效率，在实验中已经证明了这点，实验结果如表 8 所示 。\n\n![表8](../../images/shufflenetv2/table_8.png)\n\n## 4、实验\n\n**精度与 FLOPs 的关系**。很明显，我们提出的 ShuffleNet v2 模型很大程度上优于其他网络，特别是在小的计算量预算情况下。此外，我们也注意到 MobileNet v2 模型在 图像尺寸为$224 \\times 224$ 和模型计算量为 `40 MFLOPs` 量级时表现不佳，这可能是因为通道数太少的原因。相比之下，我们的设计的高效模型可以使用更多的通道数，所以并不具备此缺点。另外，如 Section 3 讨论的那样，虽然我们的模型和 DenseNet 都具有特征重用功能，但是我们的模型效率更高。\n\n**推理速度和 FLOPs/Accuracy 的关系**。本文比较了 `ShuffleNet v2、MobileNet v2、ShuffleNet v1 和 Xception` 四种模型的实际推理速度和 `FLOPs`的关系，如图 1(c)(d) 所示，在不同分辨率条件下的更多结果在附录表 1 中提供。\n\n尽管 MobileNet v1 的精度表现不佳，但是其速度快过了 SHuffleNet v2等网络。我们认为是因为 MobileNet v1 符合本文建议的原则（比如 **G3** 原则， MobileNet v1 的碎片化程度少于 ShuffleNet v2）。\n\n**与其他方法的结合**。ShuffleNet v2 与其他方法结合可以进一步提高性能。当使用 `SE` 模块时，模型会损失一定的速度，但分类的精度会提升 0.5%。卷积 block 的结构如附录 2(b)所示，对比实验结果在表 8 中。\n\n![附录图2](../../images/shufflenetv2/appendix_figure_2.png)\n\n`Sequeeze-and-Excitation(SE)` `block` 不是一个完整的网络结构，而是一个子结构（卷积 block），**通用性较强、即插即用**，可以嵌到其他分类或检测模型中，和 `ResNext`、`ShuffleNet v2` 等模型结合。`SENet` 主要是学习了 `channel` 之间的相关性，筛选出了针对通道的注意力，稍微增加了一点计算量，但是效果比较好。`SE` 其 block 结构图如下图所示。\n\n![SE](../../images/shufflenetv2/SE.png)\n\n**大型模型通用化（Generation to Large Models）**。虽然本文的消融（`ablation`）实验主要是针对轻量级网络，但是 `ShuffleNet v2` 在大型模型($FLOPs \\geq 2G$)的表现上也丝毫不逊色。**表6** 比较了`50` 层的 `ShuffleNet v2`、`ShuffleNet v1` 和 `ResNet50` 在 `ImageNet` 分类实验上的精度，可以看出同等 `FLOPs=2.3G` 条件下 ShuffleNet v2 比 v1 的精度更高，同时和 ResNet50 相比 `FLOPs` 减少 `40%`，但是精度表现更好。实验用的网络细节参考附录表`2`。\n\n![表6](../../images/shufflenetv2/table_6.png)\n\n对于很深的 ShuffleNet v2 模型（例如超过 `100` 层），我们通过添加一个 `residual path` 来轻微的修改基本的 `block` 结构，来使得模型的训练收敛更快。表 6 提供了 带 `SE` 模块的 `164` 层的 ShuffleNet v2 模型，其精度比当前最高精度的 state-of-the-art 模型 `SENet` 精度更高，同时 `FLOPs` 更少。\n\n**目标检测任务评估**。为了评估模型的泛化性能，我们使用 `Light-Head RCNN` 作为目标检测的框架，在 COCO 数据集上做了对比实验。表 7 的实验结果表明模型在 4 种不同复杂度条件下， ShuffleNet v2 做 backbone 的模型精度比其他网络更高、速度更快，全面超越其他网络。\n\n![表7](../../images/shufflenetv2/table_7.png)\n\nTable 7: Performance on COCO object detection. The input image size is 800 1200. FLOPs row lists the complexity levels at 224 224 input size. For GPU speed evaluation, the batch size is 4. We do not test ARM because the PSRoI Pooling operation needed in [34] is unavailable on ARM currently.\n比较检测任务的结果（表 7），发现精度上 `ShuffleNet v2 > Xception ≥ ShuffleNet v1 ≥ MobileNet v2`。\n比较分类任务的结果（表 8），精度等级上 `ShuffleNet v2 ≥ MobileNet v2 > ShuffeNet v1 > Xception`。\n\n![表8](../../images/shufflenetv2/table_8.png)\n\n## 5、结论\n\n我们建议对于轻量级网络设计应该考虑**直接** `metric`（例如速度 `speed`），而不是间接 `metric`（例如 `FLOPs`）。本文提出了实用的原则和一个新的网络架构-ShuffleNet v2。综合实验证了我们模型的有效性。我们希望本文的工作可以启发未来的网络架构设计可以更重视平台特性和实用性。\n> 这里的直接 `metric`，可以是inference time or latency，也可以是模型推理速度 `speed`，其意义都是一样的。\n\n## 6，个人思考\n\n分析模型的推理性能得结合具体的推理平台（常见如：英伟达 `GPU`、移动端 `ARM CPU`、端侧 `NPU` 芯片等），目前已知影响**推理性能**的因素包括: 算子计算量 `FLOPs`（参数量 `Params`）、算子内存访问代价（访存带宽）。但相同硬件平台、相同网络架构条件下， `FLOPs` 加速比与推理时间加速比成正比。\n\n举例：对于 `GPU` 平台，`Depthwise` 卷积算子实际上是使用了大量的低 `FLOPs`、高数据读写量的操作。这些具有高数据读写量的操作，加上 `GPU` 的访存带宽限制，使得模型把大量的时间浪费在了从显存中读写数据上，导致 `GPU` 的算力没有得到“充分利用”。结论来源知乎文章-[FLOPs与模型推理速度](https://zhuanlan.zhihu.com/p/122943688)。\n\n最后，目前 `AI` 训练系统已经有了一个公认的评价标准和平台-`MLPerf`，但是 `AI` 推理系统的评估，目前还没有一个公认的评价指标。`Training` 系统的性能可以使用“达到特定精度的时间”这个简单的标准来衡量。但 `Inference` 系统却很难找到一个简单的指标，`Latency`，`Throughput`，`Power`，`Cost` 等等，哪个指标合适呢？目前没有统一的标准。\n\n## 参考资料\n\n1. [Group Convolution分组卷积，以及Depthwise Convolution和Global Depthwise Convolution](https://www.cnblogs.com/shine-lee/p/10243114.html)\n2. [分组卷积和深度可分离卷积](https://linzhenyuyuchen.github.io/2020/05/09/%E5%88%86%E7%BB%84%E5%8D%B7%E7%A7%AF%E5%92%8C%E6%B7%B1%E5%BA%A6%E5%8F%AF%E5%88%86%E7%A6%BB%E5%8D%B7%E7%A7%AF/)\n3. [理解分组卷积和深度可分离卷积如何降低参数量](https://zhuanlan.zhihu.com/p/65377955)\n4. [深度可分离卷积（Xception 与 MobileNet 的点滴）](https://www.jianshu.com/p/38dc74d12fcf)\n5. [MobileNetV1代码实现](https://www.cnblogs.com/linzzz98/articles/13453810.html)\n6. [轻量级神经网络：ShuffleNetV2解读](https://www.jiqizhixin.com/articles/2019-06-03-14)\n7. [ShufflenetV2_高效网络的4条实用准则](https://zhuanlan.zhihu.com/p/42288448)\n8. [ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices](https://arxiv.org/abs/1707.01083)\n9. [MobileNetV2: Inverted Residuals and Linear Bottlenecks](https://arxiv.org/abs/1801.04381)\n10. [MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications](https://arxiv.org/abs/1704.04861)\n11. [Squeeze-and-Excitation Networks](https://openaccess.thecvf.com/content_cvpr_2018/papers/Hu_Squeeze-and-Excitation_Networks_CVPR_2018_paper.pdf)\n"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/VoVNet论文解读.md",
    "content": "- [摘要](#摘要)\n- [1，介绍](#1介绍)\n- [2，高效网络设计的影响因素](#2高效网络设计的影响因素)\n  - [2.1，内存访问代价](#21内存访问代价)\n  - [2.2，GPU计算效率](#22gpu计算效率)\n- [3，建议的方法](#3建议的方法)\n  - [3.1，重新思考密集连接](#31重新思考密集连接)\n  - [3.2，One-Shot Aggregation](#32one-shot-aggregation)\n  - [3.3，构建 VoVNet 网络](#33构建-vovnet-网络)\n- [4，实验](#4实验)\n- [5，代码解读](#5代码解读)\n- [6. 总结](#6-总结)\n- [参考资料](#参考资料)\n\n> 文章同步发于[知乎](https://www.zhihu.com/column/c_1359601708180529152)。最新版以 `github` 为主。如果看完文章有所收获，一定要先点赞后收藏。**毕竟，赠人玫瑰，手有余香**。\n\n## 摘要\n\n> `Youngwan Lee*` 作者于 `2019` 年发表的论文 An Energy and GPU-Computation Efficient Backbone Network for Real-Time Object Detection. 是对 `DenseNet` 网络推理效率低的改进版本。\n\n因为 `DenseNet` 通过用密集连接，来聚合具有不同感受野大小的中间特征，因此它在对象检测任务上表现出良好的性能。虽然特征重用（`feature reuse`）的使用，让 `DenseNet` 以少量模型参数和 `FLOPs`，也能输出有力的特征，但是使用 `DenseNet` 作为 `backbone` 的目标检测器却表现出了运行速度慢和效率低下的弊端。作者认为是密集连接(`dense connection`)带来的输入通道线性增长，从而导高内存访问成本和能耗。为了提高 `DenseNet` 的效率，作者提出一个新的更高效的网络 `VoVet`，由 `OSA`（`One-Shot Aggregation`，一次聚合）组成。`OSA` **仅在模块的最后一层聚合前面所有层的特征**，这种结构不仅继承了 `DenseNet` 的多感受野表示多种特征的优点，也解决了密集连接效率低下的问题。基于 `VoVNet` 的检测器不仅速度比 `DenseNet` 快 2 倍，能耗也降低了 1.5-4.1 倍。另外，`VoVNet` 网络的速度和效率还优于 `ResNet`，并且其对于小目标检测的性能有了显著提高。\n\n## 1，介绍\n\n随着 `CNN` 模型：`VGG`、`ResNet` 和 `DensNet` 的巨大进步，它们开始被广泛用作目标检测器的 `backbone`，用来提取图像特征。\n\n`ResNet` 和 `DenseNet` 主要的区别在于它们聚合特征的方式，`ResNet` 是通过逐元素相加（`element-wise add`）和前面特征聚合，`DenseNet` 则是通过拼接（`concatenation`）的方式。`Zhu` 等人在论文[32](https://arxiv.org/abs/1801.05895) 中认为前面的特征图携带的信息将在与其他特征图相加时被清除。换句话说，通过 `concatenation` 的方式，早期的特征才能传递下去，因为它保留了特征的原始形式（没有改变特征本身）。\n\n最近的一些工作 [25, 17, 13] 表明**具有多个感受野的抽象特征可以捕捉各种尺度的视觉信息**。因为检测任务比分类更加需要多样化尺度去识别对象，因此保留来自各个层的信息对于检测尤为重要，因为网络每一层都有不同的感受野。因此，在目标检测任务上，`DenseNet` 比 `ResNet` 有更好更多样化的特征表示。\n> 这是不是说明对于，多标签分类问题，用 VoVNet 作为 backbone，效果要比 ResNet 要好。因为前者可以实现多感受野表示特征。\n\n尽管使用 DenseNet 的检测器的参数量和 FLOPs 都比 ResNet 小，但是前者的能耗能耗和速度却更慢。这是因为，还有其他因素 FLOPs 和模型尺寸（参数量）影响能耗。\n\n首先，**内存访问代价** `MAC` 是影响能耗的关键因素。如图 1(a) 所示，因为 DenseNet 中的所有特征图都被密集连接用作后续层的输入，因此内存访问成本与网络深度成二次方增加，从而导致计算开销和更多的能耗。\n\n![OSA和密集连接](../../images/VoVNet/osa_and_dense_connections.png)\n\n从图 (a) 中可以看出，DenseBlock 中的每一层的输入都是前面所有层 feature map 的叠加。而图 (b)只有最后一层的输入是前面所有层 feature map 的叠加。\n\n其次，关于 **GPU 的并行计算**，DenseNet 有计算瓶颈的限制。一般来说，当操作的张量更大时，GPU 的并行利用率会更高[19,29,13]。但是为了线性增加输入通道，需要 DenseNet 采用 1×1 卷积 `bottleneck` 架构来减少输入维度和 `FLOPs`，这导致使用较小的操作数张量增加层数。作为结果就是 GPU 计算变得低效。总结就是，`bottleneck` 结构中的 $1\\times 1$ 卷积会导致 `GPU` 并行利用率。\n\n本文的目的在于将 DenseNet 改进的更高效，同时，还保留对目标检测有益的连接聚合（`concatenative aggregation`）操作。\n\n> 作者认为 DenseNet 网络 DenseBlock 中间层的密集连接（`dense connections`）会导致网络效率低下，并假设相应的密集连接是多余的。\n\n作者使用 `OSA` 模块构建了 `VoVNet` 网络，为了验证有效性，将其作为 DSOD、RefineDet 和 Mask R-CNN 的 backbone 来做对比实验。实验结果表明，基于 VoVNet 的检测器优于 DenseNet 和 ResNet，速度和能耗都更优。\n\n## 2，高效网络设计的影响因素\n\n作者认为，MobileNet v1 [8], MobileNet v2 [21], ShuffleNet v1 [31], ShuffleNet v2 [18], and Pelee 模型主要是通过使用 `DW` 卷积和 带 $1\\times 1$ 卷积的 `bottleneck` 结构来减少 `FLOPs` 和模型尺寸（参数量）。\n> 这里我觉得作者表达不严谨，因为 shufflenetv2 在论文中已经声明过，FLOPs 和模型参数量不是模型运行速度的唯一决定因素。\n\n实际上，减少 `FLOPs` 和模型大小并不总能保证减少 GPU 推理时间和实际能耗，典型的例子就是 `DenseNet` 和 `ResNet` 的对比，还有就是在 GPU 平台上， Shufflenetv2 在同等参数条件下，运行速度比 MobileNetv2 更快。这些现象告诉我们，`FLOPs` 和 模型尺寸（参数）是衡量模型实用性（`practicality`）的间接指标。为了设计更高效的网络，我们需要使用直接指标 `FPS`，除了上面说的 `FLOPs` 和模型参数量会影响模型的运行速度（`FPS`），还有以下几个因素。\n\n### 2.1，内存访问代价\n\n这个 Shufflenetv2 作者已经解释得很清楚了，本文的作者的描述基本和 Shufflenetv2 一致。我这里直接给结论：\n\n- `MAC` 对能耗的影响超过了计算量 `FLOPs` [28]。\n- 卷积层输入输出通道数相等时，`MAC` 取得最小值。\n- 即使模型参数量一致，只要 `MAC` 不同，那么模型的运行时间也是不一致的(ShuffleNetv2 有实验证明)。\n\n> 论文 [28] Designing energy-efficient convolutional neural networks using energyaware pruning.\n\n### 2.2，GPU计算效率\n\n> 其实这个内容和 shufflenetv2 论文中的 G3 原则（网络碎片化会降低 GPU 并行度）基本一致。\n\n**为提高速度而降低 `FLOPs` 的网络架构基于这样一种理念，即设备中的每个浮点运算都以相同的速度进行处理**。但是，当模型部署在 `GPU` 上时，不是这样的，因为 GPU 是并行处理机制能同时处理多个浮点运算进程。我们用 GPU 计算效率来表示 GPU 的运算能力。\n\n- 通过减少 `FLOPs` 是来加速的前提是，设备中的每个浮点运算都以相同的速度进行处理；\n- **GPU 特性**：\n  - 擅长 `parallel computation`，`tensor` 越大，`GPU` 使用效率越高。\n  - 把大的卷积操作拆分成碎片的小操作将不利于 `GPU` 计算。\n- 因此，设计 `layer` 数量少的网络是更好的选择。`MobileNet`使用额外的 1x1 卷积来减少计算量，不过这不利于 GPU 计算。\n- 为了衡量 GPU 利用率，引入有一个新指标：$FLOP/s = \\frac{FLOPs}{GPU\\ inference\\ time}$（每秒完成的计算量 `FLOPs per Second`），FLOP/s 高，则 `GPU` 利用率率也高。\n\n## 3，建议的方法\n\n### 3.1，重新思考密集连接\n\n1，**DenseNet 的优点**：\n\n在计算第 $l$ 层的输出时，要用到之前所有层的输出的 concat 的结果。这种**密集的连接使得各个层的各个尺度的特征都能被提取**，供后面的网络使用。这也是它能得到比较高的精度的原因，而且**密集的连接更有利于梯度的回传**（ResNet shorcut 操作的加强版）。\n\n2，**DenseNet 缺点**（导致了能耗和推理效率低的）：\n\n- 密集连接会增加输入通道大小，但输出通道大小保持不变，导致的输入和输出通道数都不相等。因此，DenseNet 具有具有较高的 MAC。\n- DenseNet 采用了 `bottleneck` 结构，这种结构将一个 $3\\times 3$ 卷积分成了两个计算（1x1+3x3 卷积），这带来了更多的序列计算（sequential computations），导致会降低推理速度。\n\n> 密集连接会导致计算量增加，所以不得不采用 $1\\times 1$ 卷积的 `bottleneck` 结构。\n\n图 7 的第 1 行是 DenseNet 各个卷积层之间的相互关系的大小。第 $(s,l)$ 块代表第 $s$ 层和第 $l$ 层之间这个卷积权值的平均 $L_1$ 范数（按特征图数量归一化后的 L1 范数）的大小，也就相当于是表征 $X_s$ 和 $X_l$ 之间的关系。\n\n图 2. 训练后的 DenseNet(顶部) 和 VoVNet(中间和底部) 中卷积层的滤波器权重的绝对值的平均值。像素块的颜色表示的是相互连接的网络层(i, j)的权重的平均 $L_1$ 范数（按特征图数量归一化后的 L1 范数）的值。OSA Module (x/y) 指的是 OSA 模块由 $x$ 层和 $y$ 个通道组成。\n\n![Figure2](../../images/VoVNet/Figure2.png)\n\n如图 2 顶部图所示， `Hu` 等人[9]通过评估每层输入权重归一化后的 L1 范数来说明密集连接的连通性（`connectivity`），这些值显示了前面所有层对相应层的归一化影响，1 表示影响最大，0 表示没有影响（两个层之间的权重没有关系）。\n\n> 这里重点解释下连通性的理解。两层之间的输入权重的绝对值相差越大，即 L1 越大，那么说明卷积核的权重越不一样，前面层对后面层影响越大（`connectivity`），即连通性越好（大）。从实用性角度讲，我们肯定希望相互连接的网络层的连通性越大越好（归一化后是 0~1 范围），这样我的密集连接才起作用了嘛。不然，耗费了计算量、牺牲了效率，但是连通性结果又差，那我还有必要设计成密集连接（`dense connection`）。作者通过图 2 后面的两张图也证明了**DenseBlock 模块中各个层之间的联系大部分都是没用，只有少部分是有用的，即密集连接中大部分网络层的连接是无效的**。\n\n在 Dense Block3 中，对角线附近的红色框表示中间层（`intermediate layers`）上的聚合处于活动状态，但是分类层（`classification layer`）只使用了一小部分中间特征。 相比之下，在 Dense Block1 中，过渡层（`transition layer`）很好地聚合了其大部分输入特征，而中间层则没有。\n> Dense Block3 的分类层和 Dense Block1 的过渡层都是模块的最后一层。\n\n通过前面的观察，我们先假设中间层的聚集强度和最后一层的聚集强度之间存在负相关（中间层特征层的聚合能力越好，那么最后层的聚合能力就越弱）。如果中间层之间的密集连接导致了每一层的特征之间存在相关性，则密集连接会使后面的中间层产生更好的特征的同时与前一层的特征相似，则假设成立。在这种情况下，因为这两种特征代表**冗余信息**，所以最后一层不需要学习聚合它们，从而前中间层对最终层的影响变小。\n\n因为最后一层的特征都是通过聚集（`aggregated`）所有中间层的特征而产生的，所以，我们当然希望中间层的这些特征能够互补或者相关性越低越好。因此，进一步提出假设，**相比于造成的损耗，中间特征层的 dense connection 产生的作用有限**。为了验证假设，我们重新设计了一个新的模块 `OSA`，该模块**仅在最后一层聚合块中其他层的特征（`intermediate features`）**，把中间的密集连接都去掉。\n\n### 3.2，One-Shot Aggregation\n\n为了验证我们的假设，中间层的聚合强度和最后一层的聚合强度之间存在负相关，并且密集连接是多余的，我们与 Hu 等人进行了相同的实验，实验结果是图 2 中间和底部位置的两张图。\n\n![Figure2-middle-bottom](../../images/VoVNet/Figure2-middle-bottom.png)\n\n从图 2（中间）可以观察到，随着中间层上的密集连接被剪掉，最终层中的聚合变得更加强烈。同时，蓝色的部分 (联系大部分不紧密的部分) 明显减少了很多，也就是说 OSA 模块的每个连接都是**相对有用的**。\n\n从图 2（底部）的可以观察到，OSA 模块的过渡层的权重显示出与 DenseNet 不同的模式：来自浅层的特征更多地聚集在过渡层上。由于来自深层的特征对过渡层的影响不大，我们可以在没有显着影响的情况下减少 OSA 模块的层数，得到。令人惊讶的是，使用此模块（5 层网络），我们实现了 5.44% 的错误率，与 DenseNet-40 （模块里有 12 层网络）的错误率（5.24%）相似。这意味着通过密集连接构建深度中间特征的效果不如预期（`This implies that building deep intermediate feature via dense connection is less effective than expected`）。\n\n**One-Shot Aggregation（只聚集一次）是指 OSA 模块的 concat 操作只进行一次，即只有最后一层的输入是前面所有层 feature map 的 concat（叠加）**。`OSA` 模块的结构图如图 1(b) 所示。\n\n![Figure1](../../images/VoVNet/Figure1.png)\n\n在 OSA 模块中，每一层产生两种连接，一种是通过 conv 和下一层连接，产生 `receptive field` 更大的 `feature map`，另一种是和最后的输出层相连，以聚合足够好的特征。\n\n为了验证 OSA 模块的有效性，作者使用 dense block 和 OSA 模块构成 DenseNet-40网络，使两种模型参数量一致，做对比实验。OSA 模板版本在 CIFAR-10 数据集上的精度达到了 `93.6`，和 dense block 版本相比，只下降了 `1.2%`。再根据 MAC 的公式，可知 MAC 从 3.7M 减少为 2.5M。`MAC` 的降低是因为 OSA 中的中间层具有相同大小的输入输出通道数，从而使得 MAC 可以取最小值（`lower boundary`）。\n\n因为 OSA 模块中间层的输入输出通道数一致，所以没必要使用 `bottleneck` 结构，这又进一步提高了 GPU 利用率。\n\n### 3.3，构建 VoVNet 网络\n\n因为 `OSA` 模块的多样化特征表示和效率，所以可以通过**仅堆叠几个模块**来构建精度高、速度快的 `VoVNet` 网络。基于图 2 中浅层深度更容易聚合的认识，作者认为可以配置比 `DenseNet` 具有更大通道数的但更少卷积层的 `OSA` 模块。\n\n如下图所示，分别构建了 VoVNet-27-slim，VoVNet-39， VoVNet-57。注意，其中downsampling 层是通过 3x3 stride=2 的 max pooling 实现的，conv 表示的是 Conv-BN-ReLU 的顺序连接。\n\n![VoVNet网络结构概览](../../images/VoVNet/vovnet_network_structure_overview.png)\n\n VOVNet 由 5 个阶段组成，各个阶段的输出特征大小依次降为原来的一半。VOVNet-27 前 2 个 stage 的连接图如下所示。\n\n![VOVNet-27前2个stage](../../images/VoVNet/vovnet_27_first_two_stages.jpg)\n\n## 4，实验\n\nGPU 的能耗计算公式如下：\n\n![GPU能耗计算公式](../../images/VoVNet/gpu_energy_consumption_calculation_formula.png)\n\n**实验1： VoVNet vs. DenseNet. 对比不同 backbone 下的目标检测模型性能(PASCALVOC)**\n\n![对比试验](../../images/VoVNet/comparative_experiment.png)\n\n对比指标：\n\n- Flops：模型需要的计算量\n- FPS：模型推断速度img/s\n- Params：参数数量\n- Memory footprint：内存占用\n- Enegry Efficiency：能耗\n- Computation Efficiency：GPU 计算效率（GFlops/s）\n- mAP（目标检测性能评价指标）\n\n现象与总结：\n- **现象 1**：相比于 DenseNet-67，PeleeNet 减少了 Flops，但是推断速度没有提升，与之相反，VoVNet-27-slim 稍微增加了Flops，而推断速度提升了一倍。同时，VoVNet-27-sli m的精度比其他模型都高。\n- **现象 2**：VoVNet-27-slim 的内存占用、能耗、GPU 利用率都是最高的。\n- **结论 1**：相比其他模型，**VoVNet做到了准确率和效率的均衡，提升了目标检测的整体性能**。\n\n**实验2：Ablation study on 1×1 conv bottleneck.**\n\n![bottleneck验证实验](../../images/VoVNet/bottleneck_verification_experiment.png)\n\n结论 2：可以看出，1x1 bottleneck 增加了 GPU Inference 时间，降低了 mAP，尽管它减少了参数数量和计算量。\n\n因为 1x1 bottleneck 增加了网路的总层数，需要更多的激活层，从而增加了内存占用。\n\n**实验3： GPU-Computation Efficiency.**\n\n![GPU计算效率对比实验](../../images/VoVNet/gpu_computing_efficiency_comparison_experiment.png)\n\n- 图3(a) VoVNet 兼顾准确率和 Inference 速度\n- 图3(b) VoVNet 兼顾准确率和 GPU 使用率\n- 图3(c) VoVNet 兼顾准确率和能耗\n- 图3(d) VoVNet 兼顾能耗和 GPU 使用率\n\n**实验室4：基于RefineDet架构比较VoVNet、ResNet和DenseNet**。\n\n![Table4](../../images/VoVNet/Table4.png)\n\n**结论 4**：从 COCO 数据集测试结果看，相比于 ResNet，VoVnet在 Inference 速度，内存占用，能耗，GPU 使用率和准确率上都占据优势。尽管很多时候，VoVNet 需要更多的计算量以及参数量。\n\n- 对比 DenseNet161(k=48) 和 DenseNet201(k=32)可以发现，深且”瘦“的网络，GPU 使用率更低。\n- 另外，作者发现**相比于 ResNet，VoVNet 在小目标上的表现更好**。\n\n**实验 5：Mask R-CNN from scratch.**\n\n通过替换 Mask R-CNN 的 backbone，也发现 VoVNet 在Inference 速度和准确率上优于 ResNet。\n\n![mask-rcnn上的实验结果](../../images/VoVNet/experimental_results_on_mask_rcnn.png)\n\n## 5，代码解读\n\n虽然 VoVNet 在 [CenterMask 论文](https://link.zhihu.com/?target=https%3A//arxiv.org/pdf/1911.06667.pdf) 中衍生出了升级版本 VoVNetv2，但是本文的代码解读还是针对原本的 VoVNet，代码来源[这里](https://github.com/stigma0617/VoVNet.pytorch/blob/master/models_vovnet/vovnet.py)。\n\n1，定义不同类型的卷积函数\n\n```python\ndef conv3x3(in_channels, out_channels, module_name, postfix,\n            stride=1, groups=1, kernel_size=3, padding=1):\n    \"\"\"3x3 convolution with padding. conv3x3, bn, relu的顺序组合\n\t\"\"\"\n    return [\n        ('{}_{}/conv'.format(module_name, postfix),\n            nn.Conv2d(in_channels, out_channels,\n                      kernel_size=kernel_size,\n                      stride=stride,\n                      padding=padding,\n                      groups=groups,\n                      bias=False)),\n        ('{}_{}/norm'.format(module_name, postfix),\n            nn.BatchNorm2d(out_channels)),\n        ('{}_{}/relu'.format(module_name, postfix),\n            nn.ReLU(inplace=True)),\n    ]\n\ndef conv1x1(in_channels, out_channels, module_name, postfix,\n            stride=1, groups=1, kernel_size=1, padding=0):\n    \"\"\"1x1 convolution\"\"\"\n    return [\n        ('{}_{}/conv'.format(module_name, postfix),\n            nn.Conv2d(in_channels, out_channels,\n                      kernel_size=kernel_size,\n                      stride=stride,\n                      padding=padding,\n                      groups=groups,\n                      bias=False)),\n        ('{}_{}/norm'.format(module_name, postfix),\n            nn.BatchNorm2d(out_channels)),\n        ('{}_{}/relu'.format(module_name, postfix),\n            nn.ReLU(inplace=True)),\n    ]\n```\n\n2，其中 `OSA` 模块结构的代码如下。\n\n```python\nclass _OSA_module(nn.Module):\n    def __init__(self,\n                 in_ch,\n                 stage_ch,\n                 concat_ch,\n                 layer_per_block,\n                 module_name,\n                 identity=False):\n        super(_OSA_module, self).__init__()\n\n        self.identity = identity # 默认不使用恒等映射\n        self.layers = nn.ModuleList()\n        in_channel = in_ch\n        # stage_ch: 每个 stage 内部的 channel 数\n        for i in range(layer_per_block):\n            self.layers.append(nn.Sequential(\n                OrderedDict(conv3x3(in_channel, stage_ch, module_name, i))))\n            in_channel = stage_ch\n\n        # feature aggregation\n        in_channel = in_ch + layer_per_block * stage_ch\n        # concat_ch: 1×1 卷积输出的 channel 数\n        # 也从 stage2 开始，每个 stage 最开始的输入 channnel 数\n        self.concat = nn.Sequential(\n            OrderedDict(conv1x1(in_channel, concat_ch, module_name, 'concat')))\n\n    def forward(self, x):\n        identity_feat = x\n        output = []\n        output.append(x)\n        for layer in self.layers:  # 中间所有层的顺序连接\n            x = layer(x)\n            output.append(x)\n        # 最后一层的输出要和前面所有层的 feature map 做 concat\n        x = torch.cat(output, dim=1)\n        xt = self.concat(x)\n\n        if self.identity:\n            xt = xt + identity_feat\n\n        return xt\n```\n\n3，定义 `_OSA_stage`，每个 `stage` 有多少个 `OSA` 模块，由 `_vovnet` 函数的 `block_per_stage` 参数指定。\n\n```python\nclass _OSA_stage(nn.Sequential):\n\t\"\"\"\n\tin_ch: 每个 stage 阶段最开始的输入通道数（feature map 数量）\n\t\"\"\"\n    def __init__(self,\n                 in_ch,\n                 stage_ch,\n                 concat_ch,\n                 block_per_stage,\n                 layer_per_block,\n                 stage_num):\n        super(_OSA_stage, self).__init__()\n\n        if not stage_num == 2:\n            self.add_module('Pooling',\n                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True))\n\n        module_name = f'OSA{stage_num}_1'\n        self.add_module(module_name,\n            _OSA_module(in_ch,\n                        stage_ch,\n                        concat_ch,\n                        layer_per_block,\n                        module_name))\n        for i in range(block_per_stage-1):\n            module_name = f'OSA{stage_num}_{i+2}'\n            self.add_module(module_name,\n                _OSA_module(concat_ch,\n                            stage_ch,\n                            concat_ch,\n                            layer_per_block,\n                            module_name,\n                            identity=True))\n```\n\n4，定义 `VOVNet`，\n\n```python\nclass VoVNet(nn.Module):\n    def __init__(self, \n                 config_stage_ch,\n                 config_concat_ch,\n                 block_per_stage,\n                 layer_per_block,\n                 num_classes=1000):\n        super(VoVNet, self).__init__()\n\n        # Stem module --> stage1\n        stem = conv3x3(3,   64, 'stem', '1', 2)\n        stem += conv3x3(64,  64, 'stem', '2', 1)\n        stem += conv3x3(64, 128, 'stem', '3', 2)\n        self.add_module('stem', nn.Sequential(OrderedDict(stem)))\n\n        stem_out_ch = [128]\n        # vovnet-57，in_ch_list 结果是 [128, 256, 512, 768]\n        in_ch_list = stem_out_ch + config_concat_ch[:-1]\n        self.stage_names = []\n        for i in range(4): #num_stages\n            name = 'stage%d' % (i+2)\n            self.stage_names.append(name)\n            self.add_module(name,\n                            _OSA_stage(in_ch_list[i],\n                                       config_stage_ch[i],\n                                       config_concat_ch[i],\n                                       block_per_stage[i],\n                                       layer_per_block,\n                                       i+2))\n\n        self.classifier = nn.Linear(config_concat_ch[-1], num_classes)\n\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight)\n            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.Linear):\n                nn.init.constant_(m.bias, 0)\n\n    def forward(self, x):\n        x = self.stem(x)\n        for name in self.stage_names:\n            x = getattr(self, name)(x)\n        x = F.adaptive_avg_pool2d(x, (1, 1)).view(x.size(0), -1)\n        x = self.classifier(x)\n        return x\n```\n\n5，VoVNet 各个版本的实现。vovnet57 中有 `4` 个 `stage`，每个 stage 的 OSP 模块数目依次是 [1,1,4,3]，每个 个 `stage` 内部对应的通道数都是一样的，分别是 [128, 160, 192, 224]。每个 `stage` 最后的输出通道数分别是 [256, 512, 768, 1024]，由 `concat_ch` 参数指定。\n\n所有版本的 vovnet 的 OSA 模块中的卷积层数都是 `5`。\n\n```python\ndef _vovnet(arch,\n            config_stage_ch,\n            config_concat_ch,\n            block_per_stage,\n            layer_per_block,\n            pretrained,\n            progress,\n            **kwargs):\n    model = VoVNet(config_stage_ch, config_concat_ch,\n                   block_per_stage, layer_per_block,\n                   **kwargs)\n    if pretrained:\n        state_dict = load_state_dict_from_url(model_urls[arch],\n                                              progress=progress)\n        model.load_state_dict(state_dict)\n    return model\n\ndef vovnet57(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-57 model as described in \n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet57', [128, 160, 192, 224], [256, 512, 768, 1024],\n                    [1,1,4,3], 5, pretrained, progress, **kwargs)\n\ndef vovnet39(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-39 model as described in\n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet39', [128, 160, 192, 224], [256, 512, 768, 1024],\n                    [1,1,2,2], 5, pretrained, progress, **kwargs)\n\n\ndef vovnet27_slim(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-39 model as described in\n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet27_slim', [64, 80, 96, 112], [128, 256, 384, 512],\n                    [1,1,1,1], 5, pretrained, progress, **kwargs)\n\n```\n\n## 6. 总结\n\n本文针对实时目标检测，提出了一种高效的主干网络 VoVNet。该网络通过多感受野的多样化特征表示，有效地解决了 DenseNet 的低效问题。我们提出的 One-Shot Aggregation (OSA) 技术，**通过在最终特征图中一次性聚合所有特征，解决了密集连接中输入通道逐渐增加的问题，确保输入尺寸恒定，从而降低了内存访问开销，并提升了 GPU 计算效率**。实验结果表明，基于 VoVNet 的检测器不仅轻量化表现优异，在大规模检测任务中也以更快的速度超越了基于 DenseNet 的检测器。\n\n## 参考资料\n\n- [论文笔记VovNet（专注GPU计算、能耗高效的网络结构）](https://zhuanlan.zhihu.com/p/79677425)\n- [An Energy and GPU-Computation Efficient Backbone Network\nfor Real-Time Object Detection](https://arxiv.org/abs/1904.09730)\n- [实时目标检测的新backbone网络：VOVNet](https://zhuanlan.zhihu.com/p/393740052)"
  },
  {
    "path": "3-classic_backbone/efficient_cnn/vovnet.py",
    "content": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom collections import OrderedDict\n\n\n__all__ = ['VoVNet', 'vovnet27_slim', 'vovnet39', 'vovnet57']\n\n\nmodel_urls = {\n    'vovnet39': 'https://dl.dropbox.com/s/1lnzsgnixd8gjra/vovnet39_torchvision.pth?dl=1',\n    'vovnet57': 'https://dl.dropbox.com/s/6bfu9gstbwfw31m/vovnet57_torchvision.pth?dl=1'\n}\n\n\ndef conv3x3(in_channels, out_channels, module_name, postfix,\n            stride=1, groups=1, kernel_size=3, padding=1):\n    \"\"\"3x3 convolution with padding\"\"\"\n    return [\n        ('{}_{}/conv'.format(module_name, postfix),\n            nn.Conv2d(in_channels, out_channels,\n                      kernel_size=kernel_size,\n                      stride=stride,\n                      padding=padding,\n                      groups=groups,\n                      bias=False)),\n        ('{}_{}/norm'.format(module_name, postfix),\n            nn.BatchNorm2d(out_channels)),\n        ('{}_{}/relu'.format(module_name, postfix),\n            nn.ReLU(inplace=True)),\n    ]\n\n\ndef conv1x1(in_channels, out_channels, module_name, postfix,\n            stride=1, groups=1, kernel_size=1, padding=0):\n    \"\"\"1x1 convolution\"\"\"\n    return [\n        ('{}_{}/conv'.format(module_name, postfix),\n            nn.Conv2d(in_channels, out_channels,\n                      kernel_size=kernel_size,\n                      stride=stride,\n                      padding=padding,\n                      groups=groups,\n                      bias=False)),\n        ('{}_{}/norm'.format(module_name, postfix),\n            nn.BatchNorm2d(out_channels)),\n        ('{}_{}/relu'.format(module_name, postfix),\n            nn.ReLU(inplace=True)),\n    ]\n\n\nclass _OSA_module(nn.Module):\n    def __init__(self,\n                 in_ch,\n                 stage_ch,\n                 concat_ch,\n                 layer_per_block,\n                 module_name,\n                 identity=False):\n        super(_OSA_module, self).__init__()\n\n        self.identity = identity\n        self.layers = nn.ModuleList()\n        in_channel = in_ch\n        for i in range(layer_per_block):\n            self.layers.append(nn.Sequential(\n                OrderedDict(conv3x3(in_channel, stage_ch, module_name, i))))\n            in_channel = stage_ch\n\n        # feature aggregation\n        in_channel = in_ch + layer_per_block * stage_ch\n        self.concat = nn.Sequential(\n            OrderedDict(conv1x1(in_channel, concat_ch, module_name, 'concat')))\n\n    def forward(self, x):\n        identity_feat = x\n        output = []\n        output.append(x)\n        for layer in self.layers:\n            x = layer(x)\n            output.append(x)\n\n        x = torch.cat(output, dim=1)\n        xt = self.concat(x)\n\n        if self.identity:\n            xt = xt + identity_feat\n\n        return xt\n\n\nclass _OSA_stage(nn.Sequential):\n    def __init__(self,\n                 in_ch,\n                 stage_ch,\n                 concat_ch,\n                 block_per_stage,\n                 layer_per_block,\n                 stage_num):\n        super(_OSA_stage, self).__init__()\n\n        if not stage_num == 2:\n            self.add_module('Pooling',\n                nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True))\n\n        module_name = f'OSA{stage_num}_1'\n        self.add_module(module_name,\n            _OSA_module(in_ch,\n                        stage_ch,\n                        concat_ch,\n                        layer_per_block,\n                        module_name))\n        for i in range(block_per_stage-1):\n            module_name = f'OSA{stage_num}_{i+2}'\n            self.add_module(module_name,\n                _OSA_module(concat_ch,\n                            stage_ch,\n                            concat_ch,\n                            layer_per_block,\n                            module_name,\n                            identity=True))\n\n\nclass VoVNet(nn.Module):\n    def __init__(self, \n                 config_stage_ch,\n                 config_concat_ch,\n                 block_per_stage,\n                 layer_per_block,\n                 num_classes=1000):\n        super(VoVNet, self).__init__()\n\n        # Stem module\n        stem = conv3x3(3,   64, 'stem', '1', 2)\n        stem += conv3x3(64,  64, 'stem', '2', 1)\n        stem += conv3x3(64, 128, 'stem', '3', 2)\n        self.add_module('stem', nn.Sequential(OrderedDict(stem)))\n\n        stem_out_ch = [128]\n        in_ch_list = stem_out_ch + config_concat_ch[:-1]\n        self.stage_names = []\n        for i in range(4): #num_stages\n            name = 'stage%d' % (i+2)\n            self.stage_names.append(name)\n            self.add_module(name,\n                            _OSA_stage(in_ch_list[i],\n                                       config_stage_ch[i],\n                                       config_concat_ch[i],\n                                       block_per_stage[i],\n                                       layer_per_block,\n                                       i+2))\n\n        self.classifier = nn.Linear(config_concat_ch[-1], num_classes)\n\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(m.weight)\n            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.Linear):\n                nn.init.constant_(m.bias, 0)\n\n    def forward(self, x):\n        x = self.stem(x)\n        for name in self.stage_names:\n            x = getattr(self, name)(x)\n        x = F.adaptive_avg_pool2d(x, (1, 1)).view(x.size(0), -1)\n        x = self.classifier(x)\n        return x\n\n\ndef _vovnet(arch,\n            config_stage_ch,\n            config_concat_ch,\n            block_per_stage,\n            layer_per_block,\n            pretrained,\n            progress,\n            **kwargs):\n    model = VoVNet(config_stage_ch, config_concat_ch,\n                   block_per_stage, layer_per_block,\n                   **kwargs)\n    if pretrained:\n        state_dict = load_state_dict_from_url(model_urls[arch],\n                                              progress=progress)\n        model.load_state_dict(state_dict)\n    return model\n\n\ndef vovnet57(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-57 model as described in \n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet57', [128, 160, 192, 224], [256, 512, 768, 1024],\n                    [1,1,4,3], 5, pretrained, progress, **kwargs)\n\n\ndef vovnet39(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-39 model as described in\n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet39', [128, 160, 192, 224], [256, 512, 768, 1024],\n                    [1,1,2,2], 5, pretrained, progress, **kwargs)\n\n\ndef vovnet27_slim(pretrained=False, progress=True, **kwargs):\n    r\"\"\"Constructs a VoVNet-39 model as described in\n    `\"An Energy and GPU-Computation Efficient Backbone Networks\"\n    <https://arxiv.org/abs/1904.09730>`_.\n    Args:\n        pretrained (bool): If True, returns a model pre-trained on ImageNet\n        progress (bool): If True, displays a progress bar of the download to stderr\n    \"\"\"\n    return _vovnet('vovnet27_slim', [64, 80, 96, 112], [128, 256, 384, 512],\n                    [1,1,1,1], 5, pretrained, progress, **kwargs)"
  },
  {
    "path": "3-classic_backbone/shufflenetv2_expr.py",
    "content": "import torch\nimport torch.nn as nn\nimport time\n\n# 生成随机数据进行测试\ndef benchmark(model, input_tensor, batch_size, device, num_iterations=100):\n    model.to(device)\n    input_tensor = input_tensor.to(device)\n    \n    # 确保模型处于评估模式\n    model.eval()\n    \n    # 预热 GPU\n    for _ in range(10):\n        with torch.no_grad():\n            model(input_tensor)\n    \n    start_time = time.time()\n    for _ in range(num_iterations):\n        with torch.no_grad():\n            output = model(input_tensor)\n    \n    elapsed_time = time.time() - start_time\n    images_per_sec = batch_size * num_iterations / elapsed_time\n    return images_per_sec\n\n# 定义卷积块\nclass ConvBlock(nn.Module):\n    def __init__(self, c1, c2):\n        super(ConvBlock, self).__init__()\n        self.conv1 = nn.Conv2d(c1, c2, kernel_size=3, stride=1, padding=1)\n        self.conv2 = nn.Conv2d(c2, c1, kernel_size=3, stride=1, padding=1)\n        self.relu = nn.ReLU(inplace=True)\n\n    def forward(self, x):\n        x = self.relu(self.conv1(x))  # 第一层卷积\n        x = self.relu(self.conv2(x))  # 第二层卷积\n        return x\n\n# 构建由 10 个卷积块组成的网络\nclass ConvNet(nn.Module):\n    def __init__(self, c1, c2, num_blocks=10):\n        super(ConvNet, self).__init__()\n        self.blocks = nn.Sequential(*[ConvBlock(c1, c2) for _ in range(num_blocks)])\n\n    def forward(self, x):\n        return self.blocks(x)\n\n# 配置参数\ninput_size = (56, 56)  # 输入图片的大小\nbatch_sizes = [1, 2, 4]  # 不同的 batch 大小\ndevice = 'cuda' if torch.cuda.is_available() else 'cpu'  # 使用 GPU 还是 CPU\n\n# 四种通道配置 (c1:c2) 比例\nchannel_configs = {\n    \"1:1\": (128, 128),\n    \"1:2\": (90, 180),\n    \"1:6\": (52, 312),\n    \"1:12\": (36, 432)\n}\n\n# 逐一测试每种通道配置和不同 batch 大小的性能\nfor ratio, (c1, c2) in channel_configs.items():\n    print(f\"Testing 3x3 conv lsyers ratio {ratio} with channels ({c1}, {c2})\")\n    model = ConvNet(c1, c2).to(device)\n    \n    for batch_size in batch_sizes:\n        input_tensor = torch.randn(batch_size, c1, input_size[0], input_size[1])\n        images_per_sec = benchmark(model, input_tensor, batch_size, device=device)\n        print(f\"Batch size {batch_size}, Images/sec: {images_per_sec:.2f}\")"
  },
  {
    "path": "3-classic_backbone/经典backbone总结.md",
    "content": "## 目录\n- [目录](#目录)\n- [VGG](#vgg)\n- [ResNet](#resnet)\n- [Inceptionv3](#inceptionv3)\n- [Resnetv2](#resnetv2)\n- [ResNeXt](#resnext)\n- [Darknet53](#darknet53)\n- [DenseNet](#densenet)\n- [CSPNet](#cspnet)\n- [VoVNet](#vovnet)\n- [一些结论](#一些结论)\n- [参考资料](#参考资料)\n\n## VGG\n\n`VGG`网络结构参数表如下图所示。\n\n![VGG](../images/backbone/VGG.png)\n\n## ResNet\n\n`ResNet` 模型比 `VGG` 网络具有更少的滤波器数量和更低的复杂性。 比如 `Resnet34` 的 `FLOPs` 为 `3.6G`，仅为 `VGG-19` `19.6G` 的 `18%`。\n> 注意，论文中算的 `FLOPs`，把乘加当作 `1` 次计算。\n\n`ResNet` 和 `VGG` 的网络结构连接对比图，如下图所示。\n\n![resnet](../images/backbone/resnet.png)\n\n不同层数的 `Resnet` 网络参数表如下图所示。\n\n![resnet网络参数表](../images/backbone/resnet_network_parameter_table.png)\n\n> 看了后续的 `ResNeXt`、`ResNetv2`、`Densenet`、`CSPNet`、`VOVNet` 等论文，越发觉得 `ResNet` 真的算是 `Backone` 领域划时代的工作了，因为它让**深层**神经网络可以训练，基本解决了深层神经网络训练过程中的梯度消失问题，并给出了系统性的解决方案（两种残差结构），即系统性的让网络变得更“深”了。而让网络变得更“宽”的工作，至今也没有一个公认的最佳方案（`Inception`、`ResNeXt` 等后续没有广泛应用），难道是因为网络变得“宽”不如“深”更重要，亦或是我们还没有找到一个更有效的方案。\n\n## Inceptionv3\n\nInception v3 是一种图像识别模型，经证实可以对 ImageNet 数据集实现 78.1% 以上的准确率。该模型是数年来多位研究人员提出的诸多想法积淀的成果。它以 Szegedy 等人发表的《Rethinking the Inception Architecture for Computer Vision》原创性论文为理论依据。\n\n模型本身由对称和非对称构建块组成，包括卷积、平均池化、最大池化、串联、丢弃、全连接层。批量归一化也在模型中广泛应用，同时用于激活输入。损失是通过 Softmax 计算的。\n\n以下是该模型的简要图示：\n\n![inceptionv3 model](../images/backbone/inceptionv3onc--oview.png)\n\n常见的一种 `Inception Modules` 结构如下：\n\n![Inception模块](../images/backbone/inception_module.jpg)\n\n论文地址 [https://arxiv.org/pdf/1512.00567.pdf](https://arxiv.org/pdf/1512.00567.pdf)。\n## Resnetv2\n\n作者总结出**恒等映射形式的快捷连接和预激活对于信号在网络中的顺畅传播至关重要**的结论。\n\n## ResNeXt\n\n`ResNeXt` 的卷积block 和 `Resnet` 对比图如下所示。\n\n![resnext的卷积block和resnet的对比图](../images/backbone/comparison_of_resnext_s_convolutional_block_and_resnet.png)\n\nResNeXt 和 Resnet 的模型结构参数对比图如下图所示。\n\n![resnext的结构参数和resnet的对比图](../images/backbone/comparison_of_resnext_s_structural_parameters_and_resnet.png)\n\n## Darknet53\n\n`Darknet53` 模型结构连接图，如下图所示。\n\n![darknet53](../images/backbone/darknet53.png)\n\n## DenseNet\n\n> 作者 `Gao Huang` 于 `2018` 年发表的论文 `Densely Connected Convolutional Networks`。\n\n**在密集块（`DenseBlock`）结构中，每一层都会将前面所有层 `concate` 后作为输入**。`DenseBlock`（类似于残差块的密集块结构）结构的 `3` 画法图如下所示：\n\n![3种DenseNet结构画法](../images/backbone/3_densenet_structure_drawing_methods.png)\n\n可以看出 `DenseNet` 论文更侧重的是 `DenseBlock` 内各个卷积层之间的密集连接（`dense connection`）关系，另外两个则是强调每层的输入是前面所有层 feature map 的叠加，反映了 feature map 数量的变化。\n\n## CSPNet\n\n**`CSPDenseNet` 的一个阶段是由局部密集块和局部过渡层组成（`a partial dense block and a partial transition layer`）**。\n\n![Figure3几种不同形式的CSP](../images/backbone/figure_3_several_different_forms_of_csp.png)\n\n`CSP` 方法可以减少模型计算量和提高运行速度的同时，还不降低模型的精度，是一种更高效的网络设计方法，同时还能和 `Resnet`、`Densenet`、`Darknet` 等 `backbone` 结合在一起。\n\n## VoVNet\n\n**One-Shot Aggregation（只聚集一次）是指 OSA 模块的 concat 操作只进行一次，即只有最后一层($1\\times 1$ 卷积)的输入是前面所有层 feature map 的 concat（叠加）**。`OSA` 模块的结构图如图 1(b) 所示。\n\n![VoVNet](../images/backbone/VoVNet.png)\n\n在 `OSA module` 中，每一层产生两种连接，一种是通过 `conv` 和下一层连接，产生 `receptive field` 更大的 `feature map`，另一种是和最后的输出层相连，以聚合足够好的特征。通过使用 `OSA module`，`5` 层 `43` `channels` 的 `DenseNet-40` 的 `MAC` 可以被减少 `30%`（`3.7M -> 2.5M`）。\n\n基于 OSA 模块构建的各种 `VoVNet` 结构参数表如下。\n\n![各种VoVNet结构](../images/backbone/various_vovnet_structures.png)\n\n作者认为 `DenseNet` 用更少的参数与 `Flops` 而性能却比 `ResNet` 更好，主要是因为`concat` 比 `add` 能保留更多的信息。但是，实际上 `DenseNet` 却比 `ResNet`要慢且消耗更多资源。\n\n`GPU` 的计算效率：\n\n- `GPU` 特性是擅长 `parallel computation`，`tensor` 越大，`GPU` 使用效率越高。\n- 把大的卷积操作拆分成碎片的小操作将不利于 `GPU` 计算。\n- 设计 `layer` 数量少的网络是更好的选择。\n- 1x1 卷积可以减少计算量，但不利于 GPU 计算。\n\n在 CenterMask 论文提出了 VoVNetv2，其卷积模块结构图如下：\n\n![VoVNetv2](../images/backbone/VoVNetv2.png)\n\n## 一些结论\n\n- 当卷积层的输入输出通道数相等时，内存访问代价（`MAC`）最小。\n- 影响 CNN 功耗的主要因素在于内存访问代价 MAC，而不是计算量 FLOPs。\n- GPU 擅长并行计算，Tensor 越大，GPU 使用效率越高，把大的卷积操作拆分成碎片的小操作不利于 GPU 计算。\n- 1x1 卷积可以减少计算量，但不利于 GPU 计算。\n\n## 参考资料\n\n+ `VGG/ResNet/Inception/ResNeXt/CSPNet` 论文\n+ [深度学习论文: An Energy and GPU-Computation Efficient Backbone Network for Object Detection及其PyTorch](https://blog.csdn.net/shanglianlm/article/details/106482678)"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-不平衡样本的处理.md",
    "content": "- [前言](#前言)\n- [一，数据层面处理方法](#一数据层面处理方法)\n  - [1.1，数据扩充](#11数据扩充)\n  - [1.2，数据（重）采样](#12数据重采样)\n    - [数据采样方法总结](#数据采样方法总结)\n  - [1.3，类别平衡采样](#13类别平衡采样)\n- [二，算法（损失函数）层面处理方法](#二算法损失函数层面处理方法)\n  - [2.1，Focal Loss](#21focal-loss)\n  - [2.2，损失函数加权](#22损失函数加权)\n- [参考资料](#参考资料)\n\n## 前言\n\n在机器学习的经典假设中往往假设训练样本各类别数目是均衡的，但在实际场景中，训练样本数据往往都是不均衡（不平衡）的。比如在图像二分类问题中，一个极端的例子是，训练集中有 `95` 个正样本，但是负样本只有 `5` 个。这种类别数据不均衡的情况下，如果不做不平衡样本的处理，会导致模型在数目较少的类别上出现“欠学习”现象，即可能在测试集上完全丧失对负样本的预测能力。\n\n除了常见的分类、回归任务，类似图像语义分割、深度估计等像素级别任务中也是存在不平衡样本问题的。\n\n解决不平衡样本问题的处理方法一般有两种:\n\n1. 从“数据层面”入手：分为数据采样法和类别平衡采样法。\n2. 从“算法层面”入手：代价敏感方法。\n\n注意本文只介绍不平衡样本的处理思想和策略，不涉及具体代码，在实际项目中，需要针对具体任务，结合不平衡样本的处理策略来设计具体的数据集处理或损失函数代码，从而解决对应问题。\n\n## 一，数据层面处理方法\n\n数据层面的处理方法总的来说分为**数据扩充和采样法**，数据扩充会直接改变数据样本的数量和丰富度，采样法的本质是使得输入到模型的训练集样本趋向于平衡，即各类样本的数目趋向于一致。\n\n数据层面的采样处理方法主要有两种策略:\n1. **数据重采样**方法，发生在**数据预处理**阶段，会改变整体训练集的数目和分布。\n2. **类别平衡采样**方法，发生在**数据加载**阶段（这里的加载是指加载到模型中，不是指从硬盘中读取文件），通过设置采样策略来使得不同类别样本送入模型训练总的次数是近似的。\n\n### 1.1，数据扩充\n\n所谓数据不平衡，其实就是某些类别的数据量太少，那就直接增加一些呗，简单直接。如果有的选，那肯定是优先选择重新采取数据的办法了，当然大部分时候我们都没得选，这个时候最有效的办法自然是**通过数据增强来扩充数据**了。\n\n数据增强的手段有多种，常见的如下:\n\n- 水平 / 竖直翻转\n- 90°，180°，270° 旋转\n- 翻转 + 旋转(旋转和翻转其实是保证了数据特征的旋转不变性能被模型学习到，卷积层面的方法可以参考论文 `ACNet`)\n- 亮度，饱和度，对比度的随机变化\n- 随机裁剪（Random Crop）\n- 随机缩放（Random Resize）\n- 加模糊（Blurring）\n- 加高斯噪声（Gaussian Noise）\n\n值得注意的是**数据增强手段的使用必须结合具体任务而来**，除了前三种以外，其他的要慎重考虑。因为不同的任务场景下数据特征依赖不同，比如高斯噪声，在天池铝材缺陷检测竞赛中，如果高斯噪声增加不当，有些图片原本在采集的时候相机就对焦不准，导致工件难以看清，倘若再增加高斯模糊属性，部分图片样本基本就废了。\n> 参考文章 [如何针对数据不平衡做处理](http://spytensor.com/index.php/archives/45/)。\n\n虽然目前深度学习框架中都自带了一些数据增强函数，但更多更强的数据增强手段可以使用一些图像增强库，比如 `imgaug` 这个 `python` 库。\n> 模型训练过程中，pytorch 框架如何在数据构建 pipeline 阶段使用 imgaug 库可以参考文章 [数据增强-imgaug](https://datawhalechina.github.io/thorough-pytorch/%E7%AC%AC%E5%85%AD%E7%AB%A0/6.5%20%E6%95%B0%E6%8D%AE%E5%A2%9E%E5%BC%BA-imgaug.html)。\n\n### 1.2，数据（重）采样\n\n简单的数据重采样方法分为**数据上采样**（`over-sampling`、`up-sampling`，也叫数据过采样） 或 也叫数据欠采样数据下采样（`under-sampling` 、`down-sampling` ）。\n\n1，**对于样本数目较少的类别，可用数据过采样方法**（`over-sampling`），即通过复制方法使得该类图像数目增至与样本最多类的样本数一致。\n\n2，而**对于样本数较多的类别，可使用数据欠采样**（`Under-sampling`，也叫数据欠采样）方法。对于深度学习和计算机视觉领域的任务来说，下采样并不是直接随机丢弃一部分图像，正确的下采样策略是: 在批处理训练时（数据加载阶段 `dataloader`），对于样本较多的类别，严格控制每批（`batch`）随机抽取的图像数目，**使得每批读取的数据中正负样本是均衡的（类别均衡）**。以二分类任务为例，假设原始数据分布情况下每批处理训练正负样本平均数量比例为 `9:1`，如仅使用下采样策略，则可在每批随机挑选训练样本时每 `9` 个正样本只取 `1` 个作为该批训练集的正样本，负样本选择策略不变，这样可使得每批读取的训练数据中正负样本时平衡的。\n\n数据过采样和欠采样示意图如下所示。\n\n![数据过采样和欠采样](../images/imbalance/under_over.png)\n\n#### 数据采样方法总结\n\n数据过采样和欠采样本质的简单理解就是“增加图片”和“删图片”:\n- 过采样：重复正比例数据，实际上没有为模型引入更多形式数据，过分强调正比例数据，会放大正比例噪音对模型的影响。\n- 欠采样：丢弃大类别的部分数据，和过采样一样会存在过拟合的问题。\n\n**同时两种数据重采样方法都是会改变数据原始分布的，比如数据过采样增加较小类别的样本数，数据欠采样减少较大类别的样本数，有可能产生模型过拟合等问题**。\n> 这里的较小类别的意思是样本数目较少的类别，较大类别即样本数目较多的类别。\n\n以上内容都是对解决类别不平衡问题中数据采样方法的**策略描述**，但想要在实际任务中解决问题，还要求我们加深对任务（`task`）的分析、对数据的理解分析，以及要求我们有更多的数据处理、数据采样的代码经验，即良好的策略 + 熟练的工具。\n> 需要注意的是，因为仅仅使用数据上采样策略有可能会引起模型过拟合问题，所以在实际任务中，更为保险的数据采样策略哇往往是将上采样和下采样结合起来使用。\n\n### 1.3，类别平衡采样\n\n前面的数据重采样策略是着重于类别样本数量，而另一类采样策略则是直接**着重于类别本身，不改变数据总体样本数，即类别平衡采样方法**。其简单策略是把样本按类别分组，每个类别生成一个样本列表，训练过程中随机选择 1 个或几个类别，然后从每个类别所对应的样本列表中随机选择样本，这样可保证每个类别参与训练的机会比较均衡。\n\n上述类别平衡方法过于简单，实际应用中有很多限制，比如在类别数很多的多分类任务中（如 `ImageNet` 数据集）。**由此，在类别平衡采样的基础上，国内海康威视研究院提出了一种“类别重组采样”的平衡方法**。\n> **类别重组法**是在《解析卷积神经网络》这本书中看到的，可惜没在网上找到原论文和代码，但这个方法感觉还是很有用的，且也比较好复现。\n\n如下图所示，**类别重组**方法步骤如下:\n\n![类别重组法步骤示意图](../images/imbalance/class_balance1.png)\n\n1. 对原始样本的每个类别的样本分别排序好，计算每个类别的样本数目，并记录样本数最多的那个类别的样本数量 `max_num`。\n2. 基于最大样本数 `max_num` 产生一个随机数列表，然后用此列表中的随机数对各自类别的样本数求余，得到对应索引值列表 `index_list`。\n3. 根据该索引值列表 `index_list`，从该类的图像数据中提取图像，生成该类的图像随机列表。\n4. 最后吧所有类别的随机列表连接在一起后一起随机打乱次序，即可得到最终的图像列表，可以发现最终的这个图像随机列表中每个类别的样本数目是一致的（样本数较少的类别，图像会存在多次采样）。然后每轮(`epoch`)都对此列表进行遍历数据用于模型训练，如此重复。\n\n以上方法整体还是比较复现的，结合具体任务来设计代码就行，这里给出一个简单的生成一段范围为 `[1, 10]` 的随机整数列示例代码。\n\n```python\nimport random\n# 生成一段范围为[0, 9]的随机整数列表\n# sample(L, n) 函数: 从序列L中随机抽取n个元素，并将n个元素以list形式返回。\n# 也可用 random.shuffle(L) 函数原地打乱列表\nrandom_list = random.sample(range(0, 10), 10)\nprint(random_list)\n```\n\n![如何得到一个随机整数列表](../images/imbalance/get_random_integer_list.png)\n\n类别重组法对有点很明显，在设计好重组代码函数后，只需要原始图像列表即可，所有操作都在内存中在线完成，易于实现且更通用。其实仔细深究可以发现，海康提出的这个类别重组法和前面的数据采样方法是很类似的，其**本质都是通过采样（sampler）策略让类别不均衡的各类数据在每轮训练中出现的次数是一致的**。\n\n## 二，算法（损失函数）层面处理方法\n\n类别不平衡问题的本质是导致样本数目较少的类别出现“欠学习”这一机器学习现象，直观表现是较小样本的损失函数权重占比也较少。一个很自然的解决办法是增加**小样本错分的惩罚代价**，并将此代价直接体现在目标函数（损失函数）里，这就是“代价敏感”的方法。“代价敏感”方法的本质可以理解为调整模型在小类别上的注意力。\n\n### 2.1，Focal Loss\n\nFocal Loss 是在二分类问题的交叉熵（CE）损失函数的基础上引入的，主要是为了解决 `one-stage` 目标检测中正负样本比例严重失衡的问题，该损失函数**降低了大量简单负样本在训练中所占的权重**，也可理解为一种困难样本挖掘，经实践证明 `Focal Loss` 在 `one-stage` 目标检测中还是很有效的，但是在多分类中不一定有效。\n\n`Focal Loss` 作者通过在交叉熵损失函数上加上一个调整因子（`modulating factor`）$(1-p_t)^\\gamma$，把高置信度 $p$（易分样本）样本的损失降低一些。`Focal Loss` 定义如下：\n\n$$\nFL(p_t) = -(1-p_t)^\\gamma log(p_t) = \\left\\{\\begin{matrix}\n-(1-p)^\\gamma log(p), & if \\quad y=1 \\\\ \n-p^\\gamma log(1-p), &  if\\quad y=0\n\\end{matrix}\\right.\n$$\n\n`Focal Loss` 有两个性质：\n\n+ 当样本被错误分类且 $p_t$ 值较小时，调制因子接近于 `1`，`loss` 几乎不受影响；当 $p_t$ 接近于 `1`，调质因子（`factor`）也接近于 `0`，**容易分类样本的损失被减少了权重**，整体而言，相当于增加了分类不准确样本在损失函数中的权重。\n+ $\\gamma$ 参数平滑地调整容易样本的权重下降率，当 $\\gamma = 0$ 时，`Focal Loss` 等同于 `CE Loss`。 $\\gamma$ 在增加，调制因子的作用也就增加，实验证明  $\\gamma = 2$ 时，模型效果最好。\n\n直观地说，**调制因子减少了简单样本的损失贡献，并扩大了样本获得低损失的范围**。例如，当$\\gamma = 2$ 时，与 $CE$ 相比，分类为 $p_t = 0.9$ 的样本的损耗将降低 `100` 倍，而当 $p_t = 0.968$ 时，其损耗将降低 `1000` 倍。这反过来又增加了错误分类样本的重要性（对于 $pt≤0.5$ 和 $\\gamma = 2$，其损失最多减少 `4` 倍）。在训练过程关注对象的排序为正难 > 负难 > 正易 > 负易。\n\n||难|易|\n|--|--|--|\n|正|1. 正难|3. 正易，$\\gamma$ 衰减|\n|负|2. 负难，$\\alpha$ 衰减|4. 负易，$\\alpha、\\gamma$衰减|\n\n在实践中，我们通常采用带 $\\alpha$ 的 `Focal Loss`：\n\n$$\nFL(p_t) = -\\alpha (1-p_t)^\\gamma log(p_t)\n$$\n\n作者在实验中采用这种形式，发现它比非 $\\alpha$ 平衡形式（non-$\\alpha$-balanced）的精确度稍有提高。实验表明 $\\gamma$ 取 2，$\\alpha$ 取 0.25 的时候效果最佳。\n> 更多理解参考 [focal loss 论文](https://arxiv.org/pdf/1708.02002.pdf)。\n\n### 2.2，损失函数加权\n\n除了 `Focal Loss` 这种高明的损失函数策略外，针对图像分类问题，还有一种简单直接的损失函数加权方法，即**在计算损失函数过程中，对每个类别的损失做加权处理**，具体的 `PyTorch` 实现方式如下:\n\n```python\nweights = torch.FloatTensor([1, 1, 8, 8, 4]) # 类别权重分别是 1:1:8:8:4\n# pos_weight_weight(tensor): 1-D tensor，n 个元素，分别代表 n 类的权重，\n# 为每个批次元素的损失指定的手动重新缩放权重，\n# 如果你的训练样本很不均衡的话，是非常有用的。默认值为 None。\ncriterion = nn.BCEWithLogitsLoss(pos_weight=weights).cuda()\n```\n\n## 参考资料\n\n- 《解析卷积神经网络》\n- [如何针对数据不平衡做处理](http://spytensor.com/index.php/archives/45/)\n- [10 Techniques to deal with Imbalanced Classes in Machine Learning](https://www.analyticsvidhya.com/blog/2020/07/10-techniques-to-deal-with-class-imbalance-in-machine-learning/)"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-数据增强.md",
    "content": "- [一，数据增强概述](#一数据增强概述)\n- [二，opencv 图像增强-几何变换](#二opencv-图像增强-几何变换)\n- [三，pytorch 图像增强](#三pytorch-图像增强)\n- [四，imgaug 图像增强](#四imgaug-图像增强)\n- [参考资料](#参考资料)\n\n## 一，数据增强概述\n\n数据增强（也叫数据扩增）的目的是为了扩充数据和提升模型的泛化能力。有效的数据扩充不仅能扩充训练样本数量，还能增加训练样本的多样性，一方面可避免过拟合，另一方面又会带来模型性能的提升。\n\n数据增强几种常用方法有: **图像水平/竖直翻转、随机抠取、尺度变换和旋转**。其中尺度变换（`scaling`）、旋转（`rotating`）等方法用来增加卷积卷积神经网络对物体尺度和方向上的鲁棒性（`Robust`）。\n\n在此基础上，对原图或已变换的图像(或图像块)进行色彩抖动(`color jittering`)也是一种常用的数据扩充手段，即改变图像颜色的四个方面: **亮度、对比度、饱和度和色调**。色彩抖动是在 `RGB` 颜色空间对原有 `RGB` 色彩分布进行轻微的扰动，也可在 `HSV` 颜色空间尝试随机改变图像原有的饱和度和明度(即改变 `S` 和 `V` 通道的值)或对色调进行微调(小范围改变 该通道的值)。\n\n> HSV 表达彩色图像的方式由三个部分组成：\n> Hue（色调、色相）\n> Saturation（饱和度、色彩纯净度）\n> Value（明度）\n\n在机器学习管道（`pipeline`）框架中，我们需要在送入模型之前，进行数据增强，一般有两种处理方式:\n\n- **线下增强**（offline augmentation）: 适用于较小的数据集（smaller dataset）。\n- **线上增强**（online augmentation）: 适用于较大的数据集（larger datasets）。\n\n## 二，opencv 图像增强-几何变换\n\nOpenCV 提供的几何变换函数如下所示:\n\n1，**拓展缩放:** 拓展缩放，改变图像的尺寸大小\n\n`cv2.resize()`: 。常用的参数有设定图像尺寸、缩放因子和插值方法。\n\n2，**平移:** 将对象换一个位置。\n\n`cv2.warpAffine()`: 函数第一个参数是原图像，第二个参数是移动矩阵，第三个参数是输出图像大小 (width, height)。举例，如果要沿 $(x，y)$ 方向移动，移动的距离是 $(tx ，ty)$，则以下面的方式构建移动矩阵:\n$$\n\\begin{bmatrix}\n1 & 0 & t_x\\\\ \n0 & 1 & t_y\n\\end{bmatrix}\n$$\n\n3，**旋转:** 对一个图像旋转角度 $\\theta$。\n\n先使用 `cv2.getRotationMatrix2D` 函数构建旋转矩阵 $M$，再使用 `cv2.warpAffine()` 函数将对象移动位置。\n\n`getRotationMatrix2D` 函数第一个参数为旋转中心，第二个为旋转角度，第三个为旋转后的缩放因子\n\n4，**放射变换（也叫平面变换/径向变换）:** 在仿射变换中，原图中所有的平行线在结果图像中依旧平行。\n\n为了找到变换矩阵，我们需要从输入图像中得到三个点，以及它们在输出图像中的对应位置。然后使用 cv2. `getAffineTransform` 先构建一个 2x3 变换矩阵，最后该矩阵将传递给 cv2.warpAffine 函数。\n\n5，**透视变换（也叫空间变换）:** 转换之后，直线仍是直线。\n\n> 原理: 透视变换（Perspective Transformation)是指利用透视中心、像点、目标点三点共线的条件，按透视旋转定律使承影面（透视面）绕迹线（透视轴）旋转某一角度，破坏原有的投影光线束，仍能保持承影面上投影几何图形不变的变换。-来源百度百科。\n\n对于透视变换，需要先构建一个 $3\\times 3$ **变换矩阵**。要找到此变换矩阵，需要在输入图像上找 4 个点，以及它们在输出图像中的对应位置。在这 4 个点中，其中任意 3 个不共线。然后可以通过函数 `cv2.getPerspectiveTransform` 找到变换矩阵，将 `cv2.warpPerspective` 应用于此 $3\\times 3$ 变换矩阵。\n\n图像几何变换的**实例代码**如下:\n\n```python\nimport cv2\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport numpy as np\n\ndef show_images(imgs, num_rows, num_cols, titles=None, scale=8.5):\n    \"\"\"Plot a list of images.\n\n    Defined in :numref:`sec_utils`\"\"\"\n    figsize = (num_cols * scale, num_rows * scale)\n    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)\n    axes = axes.flatten()\n    for i, (ax, img) in enumerate(zip(axes, imgs)):\n        try:\n            img = np.array(img)\n        except:\n            pass\n        ax.imshow(img)\n        ax.axes.get_xaxis().set_visible(False)\n        ax.axes.get_yaxis().set_visible(False)\n        if titles:\n            ax.set_title(titles[i])\n    return axes\n  \n# Some Geometric Transformation of Images\nclass GeometricTransAug(object):\n    def __init__(self, image_path):\n        img = Image.open(image_path) # load the image\n        self.img_np = np.array(img) # convert PIL image to numpy array\n        self.rows, self.cols, self.ch = self.img_np.shape\n        self.geometry_trans_aug_visual(self.img_np)\n        \n    def resize_aug(self, img_np):\n        # 直接设置了缩放因子, 缩放原大小的2倍\n        res = cv2.resize(img_np, None, fx=2, fy=2, interpolation = cv2.INTER_CUBIC)\n        return res\n    \n    def warpAffine_aug(self, img_np):\n        # 先构建转换矩阵, 将图像像素点整体进行(100,50)位移：\n        M = np.float32([[1,0,100],[0,1,50]]) \n        res = cv2.warpAffine(img_np, M,(self.cols, self.rows))\n        return res\n    \n    def rotation_aug(self, img_np):\n        rows, cols, ch = img_np.shape\n        # 先构建转换矩阵,图像相对于中心旋转90度而不进行任何缩放。\n        M = cv2.getRotationMatrix2D((self.cols/2, self.rows/2), 90, 1) \n        res = cv2.warpAffine(img_np, M, (self.cols, self.rows))\n        return res\n    \n    def radial_trans_aug(self, img_np):\n        # 仿射变换需要从原图像中找到三个点以及他们在输出图像中的位置\n        pts1 = np.float32([[50,50],[200,50],[50,200]])\n        pts2 = np.float32([[10,100],[200,50],[100,250]])\n        # 通过 getAffineTransform 创建一个 2x3 的转换矩阵\n        M = cv2.getAffineTransform(pts1,pts2)\n\n        res = cv2.warpAffine(img_np, M, dsize = (self.cols, self.rows))\n        return res\n    \n    def perspective_trans_aug(self, img_np):\n        # 透视变换需要一个 3x3 变换矩阵\n        pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]])\n        pts2 = np.float32([[0,0],[300,0],[0,300],[300,300]])\n\n        M = cv2.getPerspectiveTransform(pts1,pts2)\n        # dsize: size of the output image.\n        res = cv2.warpPerspective(img_np, M, dsize = (300,300))\n        \n        return res\n    \n    def geometry_trans_aug_visual(self, img_np):\n        res1 = self.resize_aug(img_np)\n        res2 = self.warpAffine_aug(img_np)\n        res3 = self.rotation_aug(img_np)\n        res4 = self.radial_trans_aug(img_np)\n        res5 = self.perspective_trans_aug(img_np)\n        imgs = [res1, res2, res3, res4, res5]\n        aug_titles = [\"resize_aug\", \"warpAffine_aug\", \"rotation_aug\", \"radial_trans_aug\", \"perspective_trans_aug\"]\n        # show_images 函数前文已经给出，这里不再复制过来\n        axes = show_images(imgs, 2, 3, titles=aug_titles, scale=8.5)\n            \nif __name__ == '__main__': \n    img_path = 'Koalainputimage.jpeg'\n    geometry_trans_aug = GeometricTransAug(img_path)\n    img_np2 = geometry_trans_aug.img_np\n    print(img_np2.shape)\n```\n\n程序运行后输出的**几何变换**增强效果如下所示:\n\n![opencv几何变换增强](../images/data_augmentation/opencv_geometry_transorform.png)\n\n## 三，pytorch 图像增强\n\n在 `pytorch` 框架中，`transforms` 类提供了 `22` 个数据增强方法，对应代码在 transforms.py 文件中，它们既可以对 `PIL Image` 也能对 `torch.*Tensor` 数据类型进行增强。\n\n`api` 的详细介绍可以参考官网文档-[Transforming and augmenting images](https://pytorch.org/vision/stable/transforms.html)。本章只对 transforms 的 `22` 个方法进行简要介绍和总结。\n\n总的来说 transforms.py 中的各个预处理方法可以归纳为**四大类**:\n\n1，**裁剪**-Crop\n\n- 中心裁剪: transforms.CenterCrop \n- 随机裁剪: transforms.RandomCrop \n- 随机长宽比裁剪: transforms.RandomResizedCrop \n- 上下左右中心裁剪: transforms.FiveCrop \n- 上下左右中心裁剪后翻转: transforms.TenCrop\n\n2，**翻转和变换**-Flip and Rotations\n\n- 依概率 p 水平翻转:transforms.RandomHorizontalFlip(p=0.5)\n- 依概率 p 垂直翻转:transforms.RandomVerticalFlip(p=0.5) \n- 随机旋转:transforms.RandomRotation\n\n3，**图像变换**\n\n- resize: transforms.Resize\n- `min-max Normalization`: 对应 `torchvision.transforms.ToTensor()` 方法\n- `zero-mean Normalization`: 对应 `torchvision.transforms.Normalize()` 方法\n- 填充: transforms.Pad \n- 修改亮度、对比度和饱和度:transforms.ColorJitter \n- 转灰度图: transforms.Grayscale \n- 线性变换: transforms.LinearTransformation() \n- 仿射变换: transforms.RandomAffine\n- 依概率 `p` 转为灰度图: transforms.RandomGrayscale \n- 将数据转换为 `PILImage`: transforms.ToPILImage \n- transforms.Lambda: Apply a user-defined lambda as a transform.\n\n4，对 transforms 操作，使数据增强更灵活\n\n- `transforms.RandomChoice(transforms)`: 从给定的一系列 transforms 中选一个进行操作 \n- `transforms.RandomApply(transforms, p=0.5)`: 给一个 transform 加上概率，依概率进行操作\n- `transforms.RandomOrder`: 将 transforms 中的操作随机打乱\n\n这里 `resize` 图像增强方法为例，可视化其调整输入图像大小的效果。\n\n```python\n# 为了节省空间，这里不再列出导入相应库的代码和show_images函数\nimg_PIL = Image.open('astronaut.jpeg')\nprint(img_PIL.size)\n# if you change the seed, make sure that the randomly-applied transforms\n# properly show that the image can be both transformed and *not* transformed!\ntorch.manual_seed(0)\n# size 参数: desired output size.\nresized_imgs = [transforms.Resize(size=size)(orig_img) for size in (30, 50, 100, orig_img.size)]\nshow_images(resized_imgs, 1, 4)\n```\n\n程序运行后的输出图如下。\n\n![Resize](../images/data_augmentation/resize_output.png)\n\n\n## 四，imgaug 图像增强\n\nimgaug 是一个用于机器学习实验中图像增强的库。 它支持广泛的增强技术，允许轻松组合这些技术并以随机顺序或在多个 CPU 内核上执行它们，具有简单而强大的随机接口，不仅可以增强图像，还可以增强关键点/地标、边界框、 热图和分割图。\n\n单个输入图像的示例增强如下所示。\n\n![imgaug_example](../images/data_augmentation/imaug_example.png)\n\nimgaug 的图像增强方法如下所示。\n\n- Basics\n- Keypoints\n- Bounding Boxes\n- Heatmaps\n- Segmentation Maps and Masks\n- Stochastic Parameters: 随机参数\n- Blending/Overlaying images: 混合/叠加图像\n- Augmenters: 增强器概述\n\n各个方法的使用请参考 [imaug 官网](https://imgaug.readthedocs.io/en/latest/source/installation.html)。\n\n## 参考资料\n\n1. 《解析卷积神经网络-第5、6章》\n2. 《OpenCV-Python-Toturial-中文版》"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-数据标准化.md",
    "content": "- [前言](#前言)\n- [一，Normalization 概述](#一normalization-概述)\n  - [1.1，Normalization 定义](#11normalization-定义)\n  - [1.2，什么情况需要 Normalization](#12什么情况需要-normalization)\n  - [1.3，为什么要做 Normalization](#13为什么要做-normalization)\n  - [1.4，Data Normalization 常用方法](#14data-normalization-常用方法)\n  - [1.5，代码实现](#15代码实现)\n- [二，normalize images](#二normalize-images)\n  - [2.1，图像 normalization 定义](#21图像-normalization-定义)\n  - [2.2，图像 normalization 的好处](#22图像-normalization-的好处)\n  - [2.3，PyTorch 实践图像 normalization](#23pytorch-实践图像-normalization)\n- [参考资料](#参考资料)\n\n## 前言\n\n一般机器学习任务其工作流程可总结为如下所示 `pipeline`。\n\n![机器学习任务pipeline](../images/data_normalization/ml_pipeline.png)\n\n在工业界，**数据预处理**步骤对模型精度的提高发挥着重要作用。对于机器学习任务来说，广泛的数据预处理一般有四个阶段（**视觉任务一般只需 `Data Transformation`**）: 数据清洗(Data Cleaning)、数据整合(Data Integration)、数据转换(Data Transformation)和数据缩减(Data Reduction)。\n\n![steps_in_data_normalizationing](../images/data_normalization/steps_in_data_preprocessing.png)\n\n1，`Data Cleaning`： 数据清理是数据预处理步骤的一部分，通过填充缺失值、平滑噪声数据、解决不一致和删除异常值来清理数据。\n\n2，`Data Integration`： 用于将存在于多个源中的数据合并到一个更大的数据存储中，如数据仓库。例如，将来自多个医疗节点的图像整合起来，形成一个更大的数据库。\n\n3，`Data Transformation ` : 在完成 `Data Cleaning` 后，我们需要使用以下**数据转换策略**更改数据的值、结构或格式。\n\n- `Generalization`: 使用概念层次结构将低级或粒度数据转换为高级信息。例如将城市地址中的原始数据转化为国家等更高层次的信息。\n- `Normalization`: 操作被用于**对数据属性进行缩放，使其落在较小的范围之内（即变化到某个固定区间中）**。常见方法:\n  - `Min-max normalization`：将数据映射到 $[0,1]$ 区间。\n  - `Z-Score normalization`：把每个特征值中的所有数据，变成平均值为0，标准差为1的数据，最后为正态分布。\n  - `Decimal scaling normalization`：一种归一化技术，用于通过移动数据集中每个值的小数点，将数据转换为特定范围，通常为 $[-1, 1]$ 或 $[0, 1]$。\n\n4，`Data Reduction` 数据仓库中数据集的大小可能太大而无法通过数据分析和数据挖掘算法进行处理。一种可能的解决方案是获得数据集的缩减表示，该数据集的体积要小得多，但会产生相同质量的分析结果。常见的数据缩减策略如下:\n- `Data cube aggregation`: 数据立方体聚合是一种多维数据分析技术，包括切片（Slice）、切块（Dice）、旋转（Pivot）、钻取（Drill），主要用于在数据仓库或 OLAP（在线分析处理）中对多维数据进行聚合和分析。\n- `Dimensionality reduction`: **降维技术用于执行特征提取**。数据集的维度是指数据的属性或个体特征。该技术旨在减少我们在机器学习算法中考虑的冗余特征的数量。降维可以使用主成分分析（`PCA`）等技术来完成。\n- `Data compression`: 通过使用编码技术，数据的大小可以显著减小。\n- `Discretization`: 数据离散化用于**将具有连续性的属性划分为具有区间的数据**。这样做是因为连续特征往往与目标变量相关的可能性较小。例如，属性年龄可以离散化为 18 岁以下、18-44 岁、44-60 岁、60 岁以上等区间。\n\n对于计算机视觉任务来说，在训练 `CNN` 模型之前，对于输入样本特征数据做**标准化**（`normalization`，也叫归一化）**预处理**（`data preprocessing`）操作是最常见的步骤。\n\n\n## 一，Normalization 概述\n\n> 后续内容对 Normalization 不再使用中文翻译，是因为目前中文翻译有些歧义，根据我查阅的博客资料，翻译为“归一化”比较多，但仅供可参考。\n\n### 1.1，Normalization 定义\n\n`Normalization` 操作被用于对数据属性进行缩放，使其落在较小的范围之内（即变化到某个固定区间中），比如 [-1,1] 和 [0, 1]，简单理解就是**特征缩放**过程。很多机器学习算法都受益于 `Normalization` 操作，比如:\n\n- 通常对分类算法有用。\n- 在梯度下降等机器学习算法的核心中使用的优化算法很有用。\n- 对于加权输入的算法（如回归和神经网络）以及使用距离度量的算法（如 K 最近邻）也很有用。\n\n### 1.2，什么情况需要 Normalization\n\n当我们处理的**数据具有不同尺度（范围）**（`different scale`）时，通常就需要进行 `normalization` 操作了。\n\n数据具有不同尺度的情况会导致一个重要属性（在较低尺度上）的有效性被稀释，因为其他属性可能具有更大范围（尺度）的值，简单点理解就是**范围（`scale`）大的属性在模型当中更具优先级**，具体示例如下图所示。\n\n![different scale](../images/data_normalization/diffenent_scale.png)\n\n总结起来就是，当数据存在多个属性但其值具有不同尺度（`scale`）时，这可能会导致我们在做数据挖掘操作时数据模型表现不佳，因此这时候执行 `normalization` 操作将所有属性置于相同的尺寸内是很有必要的。\n\n### 1.3，为什么要做 Normalization\n\n1，**样本的各个特征的取值要符合概率分布**，即 $[0,1]$（也可理解为降低模型训练对**特征尺度**的敏感度）。输入数据特征取值范围和输出标签范围一样，从损失函数等高线图来分析，不做 Normalization 的训练过程会更曲折。\n\n![标准化前后的损失函数等高线图的对比](../images/data_normalization/normalization_contour_line.png) \n\n2，神经网络假设所有的输入输出数据都是**标准差为1，均值为0**，包括权重值的初始化，激活函数的选择，以及优化算法的设计。\n\n3，避免一些不必要的**数值问题**。\n\n因为激活函数 sigmoid/tanh 的非线性区间大约在 [−1.7，1.7]。意味着要使神经元的激活函数有效，线性计算输出的值的数量级应该在1（1.7所在的数量级）左右。这时如果输入较大，就意味着权值必须较小，一个较大，一个较小，两者相乘，就引起数值问题了。\n\n4，**梯度更新**。\n\n如果输出层的数量级很大，会引起损失函数的数量级很大，这样做反向传播时的梯度也就很大，这时会给梯度的更新带来数值问题。\n\n5，**学习率**。\n\n特征数据数值范围不同，正确的梯度更新方向需要的学习率也会不同（如果梯度非常大，学习率就必须非常小），即不同神经元权重 $w_1$、$w_2$ 所需的学习率也不同。因此，学习率（学习率初始值）的选择需要参考输入的范围，这样不如**直接将数据标准化，这样学习率就不必再根据数据范围作调整**。\n\n### 1.4，Data Normalization 常用方法\n\n1，`z-Score Normalization`\n\nzero-mean Normalization，有时也称为 standardization，将数据特征缩放成均值为 0，方差为 1 的分布，对应公式: \n\n$$\n{x}' = \\frac{x-mean(x)}{\\sigma}\n$$\n\n其中：\n- $x$：原始值\n- $mean(x)$：表示变量 $x$ 的均值（有些地方用 $\\mu =\\frac{1}{N}\\sum_{i=1}^{N} x_i$）\n- $\\sigma$: 表示变量的标准差（总体标准差数学定义 $\\sigma = \\sqrt{\\frac{1}{N} \\sum_{i=1}^{N}(x_i - \\mu)^2}$ ）\n- ${x}'$ 是数据缩放后的新值\n\n经过 zero-mean Normalization 操作之后的数据正态分布函数曲线图会产生如下所示转换。\n\n![Z-Score Normalization show](../images/data_normalization/z-score-1.png)\n\n其中，正态分布函数曲线中均值和标准差值的确定参考下图。\n\n![正态分布函数曲线中均值和标准差值的确定](../images/data_normalization/Normal_Distribution.png)\n\n2，`Min-Max Normalization`\n\n执行线性操作，将数据范围缩放到 $[0，1]$ 区间内，对应公式: \n\n$$\n{x}' = \\frac{x - min(x)}{max(x) - min(x)}\n$$\n\n其中 $max(x)$ 是变量最大值，$min(x)$ 是变量最小值。\n\n### 1.5，代码实现\n\n1, `z-Score Normalization` 方法既可以调用相关 python 库的 `api` 实现，也可以自己实现相关功能。\n\n以下是使用  sklearn 相关类和基于 numpy 库实现 `z-Score Normalization` 功能，并给出数据直方图对比的示例代码。\n\n> 代码不是特别规范，仅供参考，当作功能理解和实验测试用。\n\n```python\n## 输出高清图像\n%config InlineBackend.figure_format = 'retina'\n%matplotlib inline\nimport numpy as np\nfrom sklearn import preprocessing\nimport matplotlib.pyplot as plt\n%matplotlib inline\nimport seaborn as sns\n\nnp.random.seed(42)\n\nplt.figure(dpi = 200)\nplt.figure(figsize=(20, 15))\n# X_train = np.array([[ 10., -1.7,  21.4],\n#                     [ 2.4,  0.,  0.6],\n#                     [ 0.9,  1., -1.9]])\n# 生成指定 size 和 范围 [low,high) 的随机浮点数\nX_train = np.random.uniform(low=0.0, high=100.0, size = (100, 15))\n# 1, 绘制原始数据的直方图\nplt.subplot(3, 1, 1)\nplt.title(\"original data distribution\")\nsns.distplot(X_train, color='y')\n\n# 2， 应用 sklearn 库的 z-Score Normalization 类，并绘制直方图\nscaler = preprocessing.StandardScaler().fit(X_train)\nX_scaled = scaler.transform(X_train)\nplt.subplot(3, 1, 2)\nplt.title(\"z-Score Normalization by sklearn\")\nsns.distplot(X_scaled, color='r')\n\n# 3，利用 numpy 函数实现 z-Score Normalization，并绘制直方图\ndef z_Score_Normalization(data):\n    data_mat = np.array(data)\n    data_z_np = (data_mat - np.mean(data_mat, axis=0)) / np.std(data_mat, axis=0)\n    return data_z_np\n\ndata_scaled = z_Score_Normalization(X_train)\nplt.subplot(3, 1, 3)\nplt.title(\"z-Score Normalization by numpy\")\nsns.distplot(data_scaled, color='g')\n```\n\n程序输出结果如下。可以看出经过 z-Score Normalization 操作之后，原始数据的分布转化成平均值为 $\\mu=0$，标准差为 $\\sigma = 1$ 的正太分布（称为**标准正态分布**）。\n\n![z_score_normalization_3_histogram](../images/data_normalization/z_score_normalization_3_histogram.png)\n\n2, `Min-Max Normalization` 方法的实现比较简单，以下是基于 numpy 库实现 `Min-Max Normalization` 功能的示例代码：\n\n```python\n# 导入必要的库\nimport numpy as np\n# 定义数据集\nX = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\ndef Min_Max_Normalization(X):\n    # 计算数据集的最小值和最大值\n    Xmin = X.min()\n    Xmax = X.max()\n    # 计算最小-最大规范化\n    X_norm = (X - Xmin) / (Xmax - Xmin)\n    return X_norm\n# 打印结果\nprint(Min_Max_Normalization(X))\n```\n\n程序输出结果如下，可以看出原始数组数据都被缩放到 $[0, 1]$ 范围内了。\n\n![程序输出结果](../images/data_normalization/output2.png)\n\n## 二，normalize images\n\n### 2.1，图像 normalization 定义\n\n当我们使用卷积神经网络解决计算机视觉任务时，一般需要对输入图像数据做 `normalization` 来完成预处理工作，常见的图像 `normalization` 方法有两种: `min-max normalization` 和 `zero-mean normalization`。\n\n1，以单张图像的 `zero-mean Normalization` 为例，它使得图像的均值和标准差分别变为 `0.0` 和 `1.0`。因为是多维数据，与纯表格数据不同，它**首先需要从每个输入通道中减去通道平均值，然后将结果除以通道标准差**。因此可定义两种 normalization 形式如下所示:\n\n```shell\n# min-max Normalization\noutput[channel] = (input[channel] - min[channel]) / (max[channel] - min[channel])\n# zero-mean Normalization\noutput[channel] = (input[channel] - mean[channel]) / std[channel]\n```\n\n### 2.2，图像 normalization 的好处\n\n图像 `normalization` 有助于使数据处于一定范围内并**减少偏度**（`skewness`），从而**有助于模型更快更好地学习**。归一化还可以解决梯度递减和爆炸的问题。\n\n### 2.3，PyTorch 实践图像 normalization\n\n在 `Pytorch` 框架中，图像变换（image transformation）是指**将图像像素的原始值改变为新值的过程**。其中常见的 `transformation` 操作是使用 torchvision.transforms.ToTensor() 方法将图像变换为 Pytorch 张量（`tensor`），它实现了将像素范围为 [0, 255] 的 `PIL` 图像转换为形状为（C,H,W）且范围为 [0.0, 1.0] 的 Pytorch FloatTensor。另外，torchvision.transforms.normalize() 方法实现了逐 channel 的对图像进行标准化（均值变为 0，标准差变为 1）。总结如下: \n\n- `min-max Normalization`: 对应 `torchvision.transforms.ToTensor()` 方法\n- `zero-mean Normalization`: 对应 `torchvision.transforms.Normalize()` 方法，利用用均值和标准差对张量图像进行 zero-mean Normalization。\n\n`ToTensor()` 函数的语法如下:\n```shell\n\"\"\"\nConvert a ``PIL Image`` or ``numpy.ndarray`` to tensor.\nConverts a PIL Image or numpy.ndarray (H x W x C) in the range\n[0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0].\n\nArgs:\n    pic (PIL Image or numpy.ndarray): Image to be converted to tensor.\nReturns:\n    Tensor: Converted image.\n\"\"\"\n```\n\n`Normalize()` 函数的语法如下:\n\n```shell\nSyntax: torchvision.transforms.Normalize()\n\nParameter:\n    * mean: Sequence of means for each channel.\n    * std: Sequence of standard deviations for each channel.\n    * inplace: Bool to make this operation in-place.\nReturns: Normalized Tensor image.\n```\n\n`torch.mean()` 函数的语法如下（Tensor.mean() 和 torch.mean() 函数功能是一样）:\n\n```python\nSyntax: torch.mean(input, dim, keepdim=False, *, dtype=None, out=None)\n\nParameters:\n\t\t* input(Tensor): the input tensor.\n\t\t* dim(int or tuple of ints): the dimension or dimensions to reduce.\n\t\t* keepdim (bool): whether the output tensor has dim retained or not.\n\nReturns: the mean value of each row of the input tensor in the given dimension dim. If dim is a list of dimensions, reduce over all of them.\n\nIf keepdim is True, the output tensor is of the same size as input except in the dimension(s) dim where it is of size 1. Otherwise, dim is squeezed (see torch.squeeze()), resulting in the output tensor having 1 (or len(dim)) fewer dimension(s).\n```\n\n`torch.mean()` 函数实现返回给定维度 `dim` 中输入张量的平均值。关于 `dim` 参数可以这么理解，`dim` 就是返回的张量中会减少的 `dim`，不管 `dim` 是整数还是列表。如果输入是 `3` 特征数据张量，且 dim = (1,2)，则表示求每个通道的平均值。示例代码如下:\n\n```python\nimport torch\nx = torch.rand(3, 4, 8) \ny1 = x.mean((1,2)) # 等效于 torch.mean(x, (1, 2))\ny2 = x.mean(2)\nprint(y1.shape)\nprint(y2.shape)\n\"\"\"\n输出结果:\n\ttorch.Size([3])\n\ttorch.Size([3, 4])\n\"\"\"\n```\n\n在 PyTorch 中对图像执行 `zero-mean Normalization` 的步骤如下:\n\n1. 加载原图像；\n2. 使用 ToTensor() 函数将图像转换为 Tensors；\n3. 计算 Tensors 的均值和方差；\n4. 使用 Normalize() 函数执行 `zero-mean Normalization` 操作。\n\n下面给出利用 PyTorch 实践 `Normalization` 操作的详细代码和输出图。\n```python\n# import necessary libraries\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport torchvision.transforms as transforms\nimport matplotlib.pyplot as plt\n\ndef show_images(imgs, num_rows, num_cols, titles=None, scale=8.5):\n    \"\"\"Plot a list of images.\n\n    Defined in :numref:`sec_utils`\"\"\"\n    figsize = (num_cols * scale, num_rows * scale)\n    _, axes = plt.subplots(num_rows, num_cols, figsize=figsize)\n    axes = axes.flatten()\n    for i, (ax, img) in enumerate(zip(axes, imgs)):\n        try:\n            img = np.array(img)\n        except:\n            pass\n        ax.imshow(img)\n        ax.axes.get_xaxis().set_visible(False)\n        ax.axes.get_yaxis().set_visible(False)\n        if titles:\n            ax.set_title(titles[i])\n    return axes\n\ndef normalize_image(image_path):\n    img = Image.open(img_path) # load the image\n    # 1, use ToTensor function\n    transform = transforms.Compose([\n        transforms.ToTensor()\n    ])\n    img_tensor = transform(img) # transform the pIL image to tensor\n    # 2, calculate mean and std by tensor's attributes\n    mean, std = img_tensor.mean([1,2]), img_tensor.std([1,2])\n    # 3, use Normalize function\n    transform_norm = transforms.Compose([\n        transforms.ToTensor(),\n        transforms.Normalize(mean, std)\n    ])\n    img_normalized = transform_norm(img) # get normalized image\n\n    img_np = np.array(img) # convert PIL image to numpy array\n    # print array‘s shape mean and std\n    print(img_np.shape) # (height, width, channel), (768, 1024, 3)\n    print(\"mean and std before normalize:\")\n    print(\"Mean of the image:\", mean)\n    print(\"Std of the image:\", std)\n    return img_np, img_tensor, img_normalized\n\ndef convert_tensor_np(tensor):\n    img_arr = np.array(tensor)\n    img_tr = img_arr.transpose(1, 2, 0)\n    return img_tr\n\nif __name__ == '__main__': \n    img_path = 'Koalainputimage.jpeg'\n    img_np, img_tensor, img_normalized = normalize_image(img_path)\n    # transpose tensor to numpy array and shape of (3,,) to shape of (,,3)\n    img_normalized1 = convert_tensor_np(img_tensor)\n    img_normalized2 = convert_tensor_np(img_normalized)\n    show_images([img_np, img_normalized1, img_normalized2], 1, 3, titles=[\"orignal\",\"min-max normalization\", \"zero-mean normalization\"])\n```\n\n1，程序输出和两种 normalization **操作效果可视化**对比图如下所示:\n\n![normalization效果图](../images/data_normalization/normalization_visual.png)\n\n2，原图和两种 normalization 操作后的图像**像素值正态分布可视化**对比图如下所示:\n\n![normalization_dist_visual](../images/data_normalization/normalization_dist_visual.png)\n\n像素值分布可视化用的代码如下。\n\n```python\n# plot the pixel values\nplt.hist(img_np.ravel(), bins=50, density=True)\nplt.xlabel(\"pixel values\")\nplt.ylabel(\"relative frequency\")\nplt.title(\"distribution of pixels\")\n```\n\n\n## 参考资料\n\n- [A Simple Guide to Data Preprocessing in Machine Learning](https://www.v7labs.com/blog/data-preprocessing-guide)\n- [Data Normalization in Data Mining](https://www.geeksforgeeks.org/data-normalization-in-data-mining/?ref=rp)\n- [scikit-learn-6.3. Preprocessing data](https://scikit-learn.org/stable/modules/preprocessing.html#standardization-or-mean-removal-and-variance-scaling)\n- [numpy.ravel](https://numpy.org/doc/stable/reference/generated/numpy.ravel.html)\n- [05.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%AC2%E6%AD%A5%20-%20%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/05.3-%E6%A0%B7%E6%9C%AC%E7%89%B9%E5%BE%81%E6%95%B0%E6%8D%AE%E6%A0%87%E5%87%86%E5%8C%96.html)\n- [numpy np.random生成随机数](https://www.cnblogs.com/cgmcoding/p/13256376.html)\n- [How To Compute Z-scores in Python](https://cmdlinetips.com/2020/12/compute-standardized-values-z-score-python/)"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-模型可视化.md",
    "content": "## 一，模型可视化\n\n模型可视化分为 3 个方向：\n\n- 可视化权重\n- 可视化激活\n- 可视化数据\n- 可视化 feature map\n\n## 参考资料\n\n1. [Visualizing Neural Networks with the Grand Tour](https://distill.pub/2020/grand-tour/)\n2. [Multi-Label Classification and Class Activation Map on Fashion-MNIST](https://towardsdatascience.com/multi-label-classification-and-class-activation-map-on-fashion-mnist-1454f09f5925)\n3. [Feature Visualization](https://distill.pub/2017/feature-visualization/)\n4. [第六章 PyTorch 可视化模块](https://tingsongyu.github.io/PyTorch-Tutorial-2nd/chapter-6/)\n5. [Pytorch训练可视化(TensorboardX)](https://zhuanlan.zhihu.com/p/54947519)"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-正则化策略.md",
    "content": "> 本文内容参考资料为《深度学习》和《解析卷积神经网络》两本书，以及部分网络资料。\n\n## 前言\n\n机器学习中的一个核心问题是设计不仅在训练数据上表现好，并且能在新输入（测试集）上泛化好的算法。\n\n**通过对学习算法的修改，旨在显示地减少泛化误差（可能会以增大训练误差为代价）的策略被称为正则化**，**正则化是一种思想（策略）**。\n\n目前有许多正则化策略。有些策略向机器学习模型添加限制参数值的额外约束。有些策略向目标函数增加额外项来对参数值进行软约束。如果我们细心选择，这些额外的约束和惩罚可以改善模型在测试集上的表现。有时侯，这些 约束和惩罚被设计为编码特定类型的先验知识; 其他时候，这些约束和惩罚被设计为偏好简单模型，以便提高泛化能力。有时惩罚和约束对于确定一个未确定的问题是必要的。其他形式的正则化，如被称为集成的方法，则结合多个假说来解释训练数据。\n> 《深度学习》第五章介绍了**泛化、欠拟合、过拟合、偏差、方差和正则化**的基本概念。\n\n## 网络参数范数惩罚\n\n许多正则化方法通过对目标函数 $J$ 添加一个**参数范数惩罚** $\\Omega (\\theta )$，限制模型（如神经网络、线性回归或逻辑回归）的学习能力。我们将正则化后的目标函数记为 $\\tilde{J}$：\n\n$$\\tilde{J}(\\theta; X,y) = J(\\theta; X,y) + \\alpha \\Omega (\\theta )$$\n\n$\\alpha $ 为 0 则没有正则化，$\\alpha $ 越大，对应正则化惩罚越大。\n\n### L2 参数正则化\n\n`L2` 参数正则化策略通过向目标函数添加一个正则项 $\\Omega (\\theta ) = \\frac{1}{2} \\left \\| w \\right \\|_{2}^{2} $，使权重更加接近原点。在其他学术圈，`L2` 正则化也被称为岭回归或 Tikhonov 正则。\n\n\n## 参考资料\n\n- 《深度学习-第七章 深度学习中的正则化》\n- 《解析卷积神经网络-章 10 网络正则化》"
  },
  {
    "path": "4-deep_learning_alchemy/深度学习炼丹-超参数调整.md",
    "content": "- [前言](#前言)\n- [一，网络层内在参数](#一网络层内在参数)\n  - [1.1，使用 3x3 卷积](#11使用-3x3-卷积)\n  - [1.2，使用 cbr 组合](#12使用-cbr-组合)\n  - [1.3，尝试不同的权重初始化方法](#13尝试不同的权重初始化方法)\n- [二，图片尺寸与数据增强](#二图片尺寸与数据增强)\n- [三，batch size 设定](#三batch-size-设定)\n  - [3.1，背景知识](#31背景知识)\n  - [3.2，batch size 定义](#32batch-size-定义)\n  - [3.3，选择合适大小的 batch size](#33选择合适大小的-batch-size)\n  - [3.4，学习率和 batch size 关系](#34学习率和-batch-size-关系)\n- [四，学习率参数设定](#四学习率参数设定)\n  - [4.1，背景知识](#41背景知识)\n  - [4.2，什么是学习率](#42什么是学习率)\n  - [4.3，如何设置学习率](#43如何设置学习率)\n- [五，优化器选择](#五优化器选择)\n  - [5.1，优化器定义](#51优化器定义)\n  - [5.2，如何选择适合不同ml项目的优化器](#52如何选择适合不同ml项目的优化器)\n  - [5.3，PyTorch 中的优化器](#53pytorch-中的优化器)\n- [六，模型 finetune](#六模型-finetune)\n- [参考资料](#参考资料)\n\n## 前言\n\n**所谓超参数，即不是通过学习算法本身学习出来的，需要作者手动调整（可优化参数）的参数**(理论上我们也可以设计一个嵌套的学习过程，一个学习算法为另一个学习算法学出最优超参数)，卷积神经网络中常见的超参数有: 优化器学习率、训练 `Epochs` 数、批次大小 `batch_size` 、输入图像尺寸大小。\n\n一般而言，我们将训练数据分成两个不相交的子集，其中一个用于学习参数，另一个作为验证集，用于估计训练中或训练后的泛化误差，用来更新超参数。\n\n- 用于学习参数的数据子集通常仍被称为训练集（不要和整个训练过程用到的更大的数据集搞混）。\n- 用于挑选超参数的数据子集被称为验证集(`validation set`)。\n\n通常，`80%` 的训练数据用于训练，`20%` 用于验证。因为验证集是用来 “训练” 超参数的，所以**验证集的误差通常会比训练集误差小**，验证集会低估泛化误差。完成所有超参数优化后，**需要使用测试集估计泛化误差**。\n\n## 一，网络层内在参数\n\n在设计网络架构的时候，我们通常需要事先指定一些网络架构参数，比如:\n- **卷积层(`convlution`)参数**: 卷积层通道数、卷积核大小、卷积步长。\n- **池化层(`pooling`)参数**: 池化核大小、步长等。\n- **权重参数初始化**，常用的初始化方法有 `Xavier`，`kaiming` 系列；或者使用模型 `fintune` 初始化模型权重参数。\n- **网络深度**（这里特指卷积神经网络 cnn），即 `layer` 的层数；网络的深度一般决定了网络的表达（抽象）能力，网络越深学习能力越强。\n- **网络宽度**，即卷积层通道(`channel`)的数量，也是**滤波器**（3 维）的数量；网络宽度越宽，代表这一层网络能学习到更加丰富的特征。\n\n这些参数一般在设计网络架构时就已经确定下来了，参数的取值一般可以参考经典 `paper` 和一些模型训练的经验总结，比如有以下经验:\n\n- 使用 `3x3` 卷积\n- 使用 `cbr` 组合\n- 尝试不同的权重初始化方法\n\n### 1.1，使用 3x3 卷积\n\n$3\\times 3$ 卷积层是 `cnn` 的主流组件，比如提取图像特征的 `backbone` 网络中，其卷积层的卷积核大小大部分都是 $3\\times 3$。比如 vgg 和 resnet 系列网络具体参数表如下所示。\n\n![Vgg 和 resnet 参数表](../images/hyperparameters/vgg_resnet_parameters.png)\n\n### 1.2，使用 cbr 组合\n\n在 `cnn` 模型中，卷积层（`conv`）一般后接 `bn`、`relu` 层，组成 `cbr` 套件。`BN` 层（batch normalization，简称 `BN`，批规范化层）很重要，是卷积层、激活函数层一样都是 `cnn` 模型的**标配组件**，其不仅**加快了模型收敛速度，而且更重要的是在一定程度缓解了深层网络的一个难题“梯度弥散”，从而使得训练深层网络模型更加容易和稳定**。\n\n另外，模型训练好后，模型推理时的卷积层和其后的 `BN` 层可以等价转换为一个带 `bias` 的卷积层（也就是通常所谓的“吸BN”），其原理参考[深度学习推理时融合BN，轻松获得约5%的提速](https://mp.weixin.qq.com/s/P94ACKuoA0YapBKlrgZl3A)。\n> 对于 cv 领域的任务，建议无脑用 `ReLU` 激活函数。\n\n```python\n# cbr 组件示例代码\ndef convbn_relu(in_planes, out_planes, kernel_size, stride, pad, dilation):\n    return nn.Sequential(\n        nn.Conv2d(in_planes, out_planes, \n                  kernel_size=kernel_size, stride=stride, \n                  padding=dilation if dilation > 1 else pad, \n                  dilation=dilation, bias=False),\n        nn.BatchNorm2d(out_planes),\n        nn.ReLU(inplace=True)\n        )\n```\n\n### 1.3，尝试不同的权重初始化方法\n\n尝试不同的卷积核权重初始化方法。目前**常用的权重初始化方法**有 `Xavier` 和 `kaiming` 系列，pytorch 框架在 `torch.nn.init` 中提供了常用的初始化方法函数，默认是使用 `kaiming ` 均匀分布函数: `nn.init.kaiming_uniform_()`。\n\n![pytorch框架默认是使用 `kaiming ` 均匀分布函数](../images/hyperparameters/reset_parameters.png)\n\n下面是一个使用 `kaiming_normal_`（kaiming 正态分布）设置卷积层权重初始化的示例代码。\n\n```python\nimport torch\nimport torch.nn as nn\n\n# 定义一个卷积层\nconv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)\n\n# 使用He初始化方式设置卷积层的权重\nnn.init.kaiming_normal_(conv.weight, mode=\"fan_out\", nonlinearity=\"relu\")\n```\n\n使用不同的卷积层权重初始化方式，会有不同的输出效果。分别使用 `xavier_normal_` 和 `xavier_uniform_` 方法来初始化权重，并使一个输入图片经过一层卷积层，其输出效果是不同的，对比图如下所示:\n\n![不同权重初始化方式效果对比图](../images/hyperparameters/different_reset_parameters.png)\n\n## 二，图片尺寸与数据增强\n\n1，**在显存满足的情况下，一般输入图片尺寸越大，模型精度越高**！\n\n2，送入到模型的**训练数据一定要充分打乱（`shuffle`）**，这样在使用自适应学习率算法的时候，可以避免某些特征集中出现，而导致的有时学习过度、有时学习不足，使得下降方向出现偏差的问题。同时，信息论（information theor）中也曾提到: “从不相似的事件中学习总是比从相似事件中学习更具信息量”。\n\n另外，为了方便做实验对比，建议**设定好随机数种子**! 并且，模型每轮（`epoch`）训练进行前将训练数据集随机打乱（`shuffle`），确保模型不同轮数相同批次“看到”的数据是不同的。\n\n3，**数据增强（图像增强）的策略必须结合具体任务来设计**！数据增强的手段有多种，常见的如下（除了前三种以外，其他的要慎重考虑）:\n\n- 水平 / 竖直翻转\n- 90°，180°，270° 旋转\n- 翻转 + 旋转(旋转和翻转其实是保证了数据特征的旋转不变性能被模型学习到，卷积层面的方法可以参考论文 `ACNet`)\n- 亮度，饱和度，对比度的随机变化\n- 随机裁剪（Random Crop）\n- 随机缩放（Random Resize）\n- 加模糊（Blurring）\n- 加高斯噪声（Gaussian Noise）\n\n## 三，batch size 设定\n### 3.1，背景知识\n\n深度学习中经常看到 `epoch`、 `iteration` 和 `batchsize`，这三个名字的区别如下：\n\n- `batch size`：批大小。在深度学习中，一般采用 SGD 训练，即每次训练在训练集中取 batch_size 个样本训练；\n- `iteration`：1 个 iteration 等于使用 batch_size 个样本训练一次；\n- `epoch`：1 个 epoch 等于使用训练集中的全部样本训练一次；\n\n### 3.2，batch size 定义\n\n**`batch` 一般被翻译为批量，设置 `batch size` 的目的让模型在训练过程中每次选择批量的数据来进行处理。`batch size` 的直观理解就是一次训练所选取的样本数**。\n\n`batch size` 的大小会影响模型的收敛速度和优化程度。同时其也直接影响到 `GPU` 内存的使用情况，如果你的 `GPU` 内存（显存）不大，该数值最好设置小一点，否则会出现显存溢出的错误。\n\n### 3.3，选择合适大小的 batch size\n\n`batch size` 是所有超参数中最好调的一个，也是应该最早确定下来的超参数，其设置的原则就是，`batch size` 别太小，也别太大，**取中间合适值为宜**，通常最好是 2 的 n 次方，如 16, 32, 64, 128。在常见的 setting（～100 epochs），batch size 一般不会低于 `16`。\n> 设置为  2 的 n 次方的原因：计算机的 gpu 和 cpu 的 memory 都是 2 进制方式存储的，设置 2 的 n 次方可以加快计算速度。\n\n`batch size` 太小和太大的问题:\n- `batch size` 太小：每次计算的梯度不稳定，引起训练的震荡比较大，很难收敛。\n- `batch size` 太大: 虽然大的 batch size 可以减少训练时间，即收敛得快，但深度学习的**优化**（training loss 降不下去）和**泛化**（generalization gap 很大）都会出问题。（结论来源论文-[Accurate, Large Minibatch SGD:\nTraining ImageNet in 1 Hour](https://arxiv.org/pdf/1706.02677.pdf)）\n\n有论文指出 `LB`（Large batch size）之所以出现 `Generalization Gap` 问题，是因为 `LB` 训练的时候更容易收敛到 `sharp minima`，而 `SB` （Small batch size）则更容易收敛到 `flat minima`，并且 `LB` 还不容易从这些 `sharp minima` 中出来，另外，作者认为关于 `batch size` 的选择是有一个阈值的，一旦超过这个阈值，模型的质量会退化，网络的准确度大幅下降。\n\n![Flat_and_Sharp_Minima](../images/hyperparameters/Flat_and_Sharp_Minima.png)\n\n> 参考论文来源 [On Large-Batch Training for Deep Learning: Generalization Gap and Sharp Minima](https://arxiv.org/pdf/1609.04836.pdf)，该论文主要探究了深度学习中一个普遍存在的问题—使用大的batchsize训练网络会导致网络的泛化性能下降（文中称之为Generalization Gap）。\n\n另外:\n\n- 合适的 batch size 范围和训练数据规模、神经网络层数、单元数都没有显著的关系。\n- 合适的 batch size 范围主要和收敛速度、随机梯度噪音有关。\n\n> 参考知乎问答-[怎么选取训练神经网络时的Batch size?](https://www.zhihu.com/question/61607442)\n\n### 3.4，学习率和 batch size 关系\n\n`batch size` 和学习率有紧密联系，我们知道深度学习模型多采用批量随机梯度下降算法进行优化，随机梯度下降算法的原理如下:\n\n$$\nw_{t+1} = w_{t} - \\eta \\frac{1}{n} \\sum_{x\\in\\beta} \\nabla l(x,w_{t})\n$$\n\n$n$ 是批量大小(batchsize)，$\\eta$ 是学习率(learning rate)。从随机梯度下降算法（SGD），可知道除了梯度本身，这两个因子直接决定了模型的权重更新，从优化本身来看它们是影响模型性能收敛最重要的参数。\n\n**学习率（`learning rate`）直接影响模型的收敛状态，`batch size` 则影响模型的泛化性能**，两者又是分子分母的直接关系，相互也可影响，因此这一次来详述它们对模型性能的影响。\n> 参考来源-[【AI不惑境】学习率和batchsize如何影响模型的性能？](https://zhuanlan.zhihu.com/p/64864995)\n\n## 四，学习率参数设定\n\n### 4.1，背景知识\n\n反向传播指的是计算神经⽹络参数梯度的⽅法。总的来说，反向传播依据微积分中的链式法则，沿着从输出层到输⼊层的顺序，依次计算并存储⽬标函数有关神经⽹络各层的中间变量以及参数的梯度。\n\n前向传播：输入层-->输出层；反向传播：输出层-->输入层。\n### 4.2，什么是学习率\n\n现阶段的所有深度神经网络的参数都是由 `BP`（反向传播）算法训练得到的，而 `BP` 算法是**基于梯度下降（`gradient desent`）策略，以目标的负梯度方向对参数进行调整的**，以下公式描述了这种关系。\n\n```shell\nnew_weight = existing_weight — learning_rate * gradient\n```\n\n更细致点，权重参数更新公式如下。\n\n$$\\theta_1 := \\theta_1  - \\alpha \\frac{\\partial}{\\partial \\theta_1}J(\\theta_1)$$\n\n> 这里套用吴恩达机器学习课程的梯度下降公式，也可参考《机器学习》书中的公式 (5.6)，虽然表达式写法不一样，但其意义是一样的。\n\n式中 $\\alpha$ 是**学习率参数**，$\\theta$ 是待更新的权重参数。学习率范围为 $\\alpha \\in (0,1)$ 控制着算法每一轮迭代中更新的步长，很明显可得，**若学习率太大则容易振荡导致不收敛，太小则收敛速度又会过慢（即损失函数的变化速度过慢）**。虽然使用低学习率可以确保我们不会错过任何局部极小值，但也意味着我们将花费更长的时间来进行收敛，特别是在被困在高原区域的情况下。\n\n![采用小学习速率（顶部）和大学习速率（底部）的梯度下降](../images/hyperparameters/lr_gradient.png)\n\n> 采用小学习速率（顶部）和大学习速率（底部）的梯度下降。图来源：Coursera 上吴恩达（Andrew Ng）的机器学习课程。\n\n因此可以说，**学习速率是指导我们如何通过损失函数的梯度调整网络权重的重要超参数 !**\n\n### 4.3，如何设置学习率\n\n训练 `CNN` 模型的过程中，关于如何设置学习率，有两个原则可以遵守:\n\n1. 模型训练开始时的**初始学习率不宜过大**，`cv` 类模型以 `0.01` 和 `0.001` 为宜；\n2. 模型训练过程中，**学习率应随轮数（`epochs`）增加而衰减**。\n\n除以上固定规则的方式之外，**还有些经验可以参考**: \n\n1. 对于图像分类任务，使用 `finetune` 方式训练模型，训练过程中，冻结层的不需要过多改变参数，因此需要设置较小的学习率，更改过的分类层则需要以较大的步子去收敛，学习率往往要设置大一点。（来源-[pytorch 动态调整学习率](http://spytensor.com/index.php/archives/32/)）\n2. 寻找理想学习率或诊断模型训练学习率是否合适时也可借助模型训练曲线(learning curve)的帮助。下图展示了不同大小的学习率下损失函数的变化情况，图来自于 `cs231n`。\n\n![不同学习率下训练损失值随训练轮数增加呈现的状态](../images/hyperparameters/lr_loss.png)\n\n以上是理论分析，但在实际应用中，以 `pytorch` 框架为例，`pyTorch` 提供了六种学习率调整方法，可分为三大类，分别是:\n\n1. **有序调整**： 按照一定规律有序进行调整，这一类是最常用的，分别是等间隔下降(`Step`)，\n按需设定下降间隔(`MultiStep`)，指数下降(`Exponential`)和 `CosineAnnealing`。这四种方法的调整时机都是人为可控的，也是训练时常用到的。\n1. **自适应调整**: 如依据训练状况伺机调整 `ReduceLROnPlateau` 方法。该法通过监测某一指标的变化情况，当该指标不再怎么变化的时候，就是调整学习率的时机，因而属于自适应的调整。\n2. **自定义调整**: 自定义调整 `Lambda`。Lambda 方法提供的调整策略**十分灵活**，我们可以为不同的层设定不同的学习率调整方法，这在 fine-tune 中十分有用，我们不仅可为不同的层设定不同的学习率，还可以为其设定不同的学习率调整策略，简直不能更棒了!\n\n常见的学习率调整方法有: \n- `lr_scheduler.StepLR`: 等间隔调整学习率。调整倍数为 `gamma` 倍，调整间隔为 `step_size`。\n- `lr_scheduler.MultiStepLR`: 按设定的间隔调整学习率。适合后期使用，通过观察 loss 曲线，手动定制学习率调整时机。\n- `lr_scheduler.ExponentialLR`: 按指数衰减调整学习率，调整公式: $lr = lr * gamma^{epoch}$\n- `lr_scheduler.CosineAnnealingLR`: 以余弦函数为周期，并在每个周期最大值时重新设置学习率。\n- `lr_scheduler.ReduceLROnPlateau`: 当某指标不再变化(下降或升高)，调整学习率（非常实用的策略）。\n- `lr_scheduler.LambdaLR`: 为不同参数组设定不同学习率调整策略。\n\n学习率调整方法类的详细参数及类方法定义，请参考 pytorch 官方库文档-[torch.optim](https://pytorch.org/docs/stable/optim.html#torch.optim.Optimizer)。\n\n注意，`PyTorch 1.1.0` 之后版本，学习率调整策略的设定必须放在优化器设定的后面! 构建一个优化器，首先需要为它指定一个待优化的参数的可迭代对象，然后设置特定于优化器的选项，比如学习率、权重衰减策略等。\n> 在 PyTorch 1.1.0 之前，学习率调度器应该在优化器更新之前被调用；1.1.0 以打破 BC 的方式改变了这种行为。 如果在优化器更新（调用 optimizer.step()）之前使用学习率调度器（调用 scheduler.step()），后果是将跳过学习率调度的第一个值。\n\n使用指数级衰减的学习率调整策略的模板代码如下。\n\n```python\nimport torchvision.models as models\nimport torch.optim as optim\nmodel = models.resnet50(pretrained=False)\n\noptimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 构建优化器，lr 是初始学习率\nscheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9) # 设定学习率调整策略\n\nfor epoch in range(20):\n    for input, target in dataset:\n        optimizer.zero_grad()\n        output = model(input)\n        loss = loss_fn(output, target)\n        loss.backward()\n        optimizer.step()\n    scheduler.step()\n    print_lr(is_verbose=true) # pytorch 新版本可用，1.4及以下版本不可用\n```\n## 五，优化器选择\n\n### 5.1，优化器定义\n\n优化器（优化算法）优化的是神经元参数的取值 $(w、b)$。优化过程如下：假设 $\\theta$ 表示神经网络中的参数，$J(\\theta)$ 表示在给定的参数取值下，训练数据集上损失函数的大小（包括正则化项），则优化过程即为寻找某一参数 $\\theta$，使得损失函数 $J(\\theta)$ 最小。\n\n在完成数据预处理、数据增强，模型构建和损失函数确定之后，深度学习任务的数学模型也就确定下来了，接下来自然就是选择一个合适的优化器(`Optimizer`)对该深度学习模型进行优化（优化器选择好后，选择合适的学习率调整策略也很重要）。\n\n### 5.2，如何选择适合不同ml项目的优化器\n\n选择优化器的问题在于**没有一个可以解决所有问题的单一优化器**。实际上，优化器的性能高度依赖于设置。所以，优化器选择的本质其实是: **哪种优化器最适合自身项目的特点？**\n\n深度卷积神经网络通常采用随机梯度下降类型的优化算法进行模型训练和参数求解。最为常用且经典的优化器算法是 （基于动量的）随机梯度下降法 `SGD（stochastic gradient descent）` 和 `Adam` 法，其他常见的优化器算法有 `Nesterov` 型动量随机下降法、`Adagrad` 法、`Adadelta` 法、`RMSProp` 法。\n\n优化器的选择虽然没有通用的准则，但是也还是有些经验可以总结的:\n\n- `SGD` 是最常见的神经网络优化方法，收敛效果较稳定，但是收敛速度过慢。\n- `Adam` 等自适应学习率算法对于稀疏数据具有优势，且且收敛速度很快，但是收敛效果不稳定（容易跳过全局最优解）。\n\n下表 1 概述了几种优化器的优缺点，通过下表可以尝试找到与数据集特征、训练设置和项目目标相匹配的优化器。\n\n某些优化器在具有稀疏特征的数据上表现出色，而其他优化器在将模型应用于以前未见过的数据时可能表现更好。一些优化器在大批量（`batch_size` 设置较大）下工作得很好，而而另一些优化器会在泛化不佳的情况下收敛到极小的最小值。\n\n![Summary of popular optimizers highlighting their strengths and weaknesses](../images/hyperparameters/optimizers_summary.png)\n\n> 表格来源 [Which Optimizer should I use for my ML Project?](https://www.lightly.ai/post/which-optimizer-should-i-use-for-my-machine-learning-project)\n\n网络上有种 `tricks` 是将 `SGD` 和 `Adam` 组合使用，先用 `Adam` 快速下降，再用 `SGD` 调优。但是这种策略也面临两个问题: 什么时候切换优化器和切换后的 SGD 优化器使用什么样的学习率？论文 `SWATS` [Improving Generalization Performance by Switching from Adam to SGD](https://arxiv.org/abs/1712.07628)给出了答案，感兴趣的读者可以深入阅读下 `paper`。\n\n### 5.3，PyTorch 中的优化器\n\n以 `Pytorch` 框架为例，PyTorch 中所有的优化器(如: optim.Adadelta、optim.SGD、optim.RMSprop 等)均是 `Optimizer` 的子类，Optimizer 中也定义了一些常用的方法:\n\n- `zero_grad()`: 将梯度清零。\n- `step(closure)`: 执行一步权值更新, 其中可传入参数 closure(一个闭包)。\n- `state_dict()`: 获取模型当前的参数，以一个有序字典形式返回，key 是各层参数名，value 就是参数。\n- `load_state_dict(state_dict)`: 将 state_dict 中的参数加载到当前网络，常用于模型 `finetune`。\n- `add_param_group(param_group)`: 给 optimizer 管理的参数组中增加一组参数，可为该组参数定制 lr, momentum, weight_decay 等，在 finetune 中常用。\n\n优化器设置和使用的模板代码如下:\n\n```python\n# optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)\n# 指定每一层的学习率\noptim.SGD([\n            {'params': model.base.parameters()},\n            {'params': model.classifier.parameters(), 'lr': 1e-3}\n          ], lr=1e-2, momentum=0.9\n        )\nfor input, target in dataset:\n    optimizer.zero_grad()\n    output = model(input)\n    loss = loss_fn(output, target)\n    loss.backward()\n    optimizer.step()\n```\n\n优化器的算法原理可以参考花书第八章内容，`Pytorch` 框架优化器类的详细参数及类方法定义，请参考 pytorch 官方库文档-[torch.optim](https://pytorch.org/docs/stable/optim.html#torch.optim.Optimizer)。\n\n## 六，模型 finetune\n\n一般情况下，我们在做深度学习任务时，`backbone` 一般会用 `imagenet` 的预训练模型的权值参数作为我们自定义模型的初始化参数，这个过程称为 `finetune`，更广泛的称之为迁移学习。fintune 的本质其实就是，让我们**有一个较好的权重初始化值**。模型 `finetune` 一般步骤如下:\n- 获取预训练模型的权重（如  `imagenet` ）；\n- 使用预训练模型权重初始化我们的模型，即加载预训练模型权重参数。\n\n模型 `finetune` 一般有两种情况:\n\n1. 直接使用 `imagenet` 的 `resnet50` 预训练模型权重当作特征提取器，只改变模型最后的全连接层。代码示例如下:\n\n```python\nimport torch\nimport torchvision.models as models\n\nmodel = models.resnet50(pretrained=True)\n\n#遍历每一个参数，将其设置为不更新参数，即冻结模型所有参数\nfor param in model.parameters():\n    param.requires_grad = False\n\n# 只替换最后的全连接层， 改为训练10类，全连接层requires_grad为True\nmodel.fc = nn.Linear(2048, 10)\n\nprint(model.fc) # 这里打印下全连接层的信息\n# 输出结果: Linear(in_features=2048, out_features=10, bias=True)\n```\n\n2. 使用自定义模型结构，保存某次训练好后比较好的权重用于后续训练 `finetune`，可节省模型训练时间，示例代码如下:\n\n```python\nimport torch\nimport torchvision.models as models\n\n# 1，保存模型权重参数值\n# 假设已经创建了一个 net = Net()，并且经过训练，通过以下方式保存模型权重值\ntorch.save(net.state_dict(), 'net_params.pth')\n# 2，加载模型权重文件\n# load(): Loads an object saved with :func:`torch.save` from a file\npretrained_dict = torch.load('net_params.pth')\n# 3，初始化模型\nnet = Net() # 创建 net\nnet_state_dict = net.state_dict() # 获取已创建 net 的 state_dict\n# (可选)将 pretrained_dict 里不属于 net_state_dict 的键剔除掉:\npretrained_dict_new = {k: v for k, v in pretrained_dict.items() if k in net_state_dict}\n# 用预训练模型的参数字典 对 新模型的参数字典 net_state_dict 进行更新\nnet_state_dict.update(pretrained_dict_new)\n# 加载需要的预训练模型参数字典\nnet.load_state_dict(net_state_dict)\n```\n\n更进一步的模型 `finetune`，可以为不同网络层设置不同的学习率，请参考《PyTorch_tutorial_0.0.5_余霆嵩》第二章。\n> 模型 `finetune` 是属于迁移学习技术的一种。\n\n## 参考资料\n\n- 《解析卷积神经网络-第 11 章》\n- 《PyTorch_tutorial_0.0.5_余霆嵩》\n- 知乎问答-[怎么选取训练神经网络时的Batch size?](https://www.zhihu.com/question/61607442)\n- [batch size设置技巧](https://blog.51cto.com/u_15485092/5464376)\n- [如何选择适合不同ML项目的优化器](https://www.jiqizhixin.com/articles/2021-01-05-9)\n- [理解深度学习中的学习率及多种选择策略](https://www.jiqizhixin.com/articles/understanding-learning-rates)\n- 《深度学习》第五章-机器学习基础\n- [知乎问答-深度学习调参有哪些技巧？](https://www.zhihu.com/question/25097993)\n- [pytorch 学习笔记-3.2 卷积层](https://pytorch.zhangxiann.com/3-mo-xing-gou-jian/3.2-juan-ji-ceng#juan-ji-wang-luo-shi-li)"
  },
  {
    "path": "5-model_compression/README.md",
    "content": "## 一些参考资料\n\n1. [《人工智能系统》](https://github.com/microsoft/AI-System/tree/main/Textbook)\n2. [ncnn](https://github.com/Tencent/ncnn)\n3. [Neural Network Intelligence](https://nni.readthedocs.io/zh/stable/compression/overview.html)"
  },
  {
    "path": "5-model_compression/基于pytorch实现模型剪枝.md",
    "content": "- [一，剪枝分类](#一剪枝分类)\n  - [1.1，非结构化剪枝](#11非结构化剪枝)\n  - [1.2，结构化剪枝](#12结构化剪枝)\n  - [1.3，本地与全局修剪](#13本地与全局修剪)\n- [二，PyTorch 的剪枝](#二pytorch-的剪枝)\n  - [2.1，pytorch 剪枝工作原理](#21pytorch-剪枝工作原理)\n  - [2.2，局部剪枝](#22局部剪枝)\n    - [2.2.1，局部非结构化剪枝](#221局部非结构化剪枝)\n    - [2.2.2，局部结构化剪枝](#222局部结构化剪枝)\n    - [2.2.3，局部结构化剪枝示例代码](#223局部结构化剪枝示例代码)\n  - [2.3，全局非结构化剪枝](#23全局非结构化剪枝)\n- [三，总结](#三总结)\n- [参考资料](#参考资料)\n\n## 一，剪枝分类\n\n所谓模型剪枝，其实是一种从神经网络中移除\"不必要\"权重或偏差（weigths/bias）的模型压缩技术。关于什么参数才是“不必要的”，这是一个目前依然在研究的领域。\n\n### 1.1，非结构化剪枝\n\n非结构化剪枝（Unstructured Puning）是指修剪参数的单个元素，比如全连接层中的单个权重、卷积层中的单个卷积核参数元素或者自定义层中的浮点数（scaling floats）。其重点在于，**剪枝权重对象是随机的，没有特定结构，因此被称为非结构化剪枝**。\n\n### 1.2，结构化剪枝\n\n与非结构化剪枝相反，结构化剪枝会剪枝整个参数结构。比如，丢弃整行或整列的权重，或者在卷积层中丢弃整个过滤器（`Filter`）。\n\n### 1.3，本地与全局修剪 \n\n剪枝可以在每层（局部）或多层/所有层（全局）上进行。\n\n## 二，PyTorch 的剪枝\n\n目前 PyTorch 框架支持的权重剪枝方法有:\n\n- **Random**: 简单地修剪随机参数。\n- **Magnitude**: 修剪权重最小的参数（例如它们的 L2 范数）\n\n以上两种方法实现简单、计算容易，且可以在没有任何数据的情况下应用。\n\n### 2.1，pytorch 剪枝工作原理\n\n剪枝功能在 `torch.nn.utils.prune` 类中实现，代码在文件 torch/nn/utils/prune.py 中，主要剪枝类如下图所示。\n\n![pytorch_pruning_api_file.png](../images/pruning_code/pytorch_pruning_api_file.png)\n\n剪枝原理是基于张量（Tensor）的掩码（Mask）实现。掩码是一个与张量形状相同的布尔类型的张量，掩码的值为 True 表示相应位置的权重需要保留，掩码的值为 False 表示相应位置的权重可以被删除。\n\nPytorch 将原始参数 `<param>` 复制到名为 `<param>_original` 的参数中，并创建一个**缓冲区**来存储剪枝掩码 `<param>_mask`。同时，其也会创建一个模块级的 forward_pre_hook 回调函数（在模型前向传播之前会被调用的回调函数），将剪枝掩码应用于原始权重。\n\npytorch 剪枝的 `api` 和教程比较混乱，我个人将做了如下表格，希望能将 api 和剪枝方法及分类总结好。\n\n![pytorch_pruning_api](../images/pruning_code/pytorch_pruning_api_summary.png)\n\npytorch 中进行模型剪枝的工作流程如下：\n\n1. 选择剪枝方法（或者子类化 BasePruningMethod 实现自己的剪枝方法）。\n2. 指定剪枝模块和参数名称。\n3. 设置剪枝方法的参数，比如剪枝比例等。\n\n### 2.2，局部剪枝\n\nPytorch 框架中的局部剪枝有非结构化和结构化剪枝两种类型，值得注意的是结构化剪枝只支持局部不支持全局。\n\n#### 2.2.1，局部非结构化剪枝\n\n1，**局部非结构化剪枝**（Locall Unstructured Pruning）对应函数原型如下：\n\n```python\ndef random_unstructured(module, name, amount)\n```\n\n1，**函数功能**：\n\n用于对权重参数张量进行**非结构化**剪枝。该方法会在张量中**随机**选择一些权重或连接进行剪枝，剪枝率由用户指定。\n\n2，函数参数定义：\n\n- `module` (nn.Module): 需要剪枝的网络层/模块，例如 nn.Conv2d() 和 nn.Linear()。\n- `name `(str): 要剪枝的参数名称，比如 \"weight\" 或 \"bias\"。\n- `amount` (int or float): 指定要剪枝的数量，如果是 0~1 之间的小数，则表示剪枝比例；如果是证书，则直接剪去参数的绝对数量。比如`amount=0.2` ，表示将随机选择 20% 的元素进行剪枝。\n\n3，下面是 `random_unstructured` 函数的使用示例。\n\n```python\nimport torch\nimport torch.nn.utils.prune as prune\nconv = torch.nn.Conv2d(1, 1, 4)\nprune.random_unstructured(conv, name=\"weight\", amount=0.5)\nconv.weight\n\"\"\"\ntensor([[[[-0.1703,  0.0000, -0.0000,  0.0690],\n          [ 0.1411,  0.0000, -0.0000, -0.1031],\n          [-0.0527,  0.0000,  0.0640,  0.1666],\n          [ 0.0000, -0.0000, -0.0000,  0.2281]]]], grad_fn=<MulBackward0>)\n\"\"\"\n```\n\n可以看书输出的 conv 层中权重值有一半比例为 `0`。\n\n#### 2.2.2，局部结构化剪枝\n\n**局部结构化剪枝**（Locall Structured Pruning）有两种函数，对应函数原型如下：\n\n```python\ndef random_structured(module, name, amount, dim)\ndef ln_structured(module, name, amount, n, dim, importance_scores=None)\n```\n\n1，函数功能\n\n与非结构化移除的是连接权重不同，结构化剪枝移除的是整个通道权重。\n\n2，参数定义\n\n与局部非结构化函数非常相似，唯一的区别是您必须定义 dim 参数(ln_structured 函数多了 `n` 参数)。\n\n`n` 表示剪枝的范数，`dim` 表示剪枝的维度。\n\n对于 torch.nn.Linear：\n\n- `dim = 0`： 移除一个神经元。\n\n- `dim = 1`：移除与一个输入的所有连接。\n\n对于 torch.nn.Conv2d：\n\n- `dim = 0`(Channels) : 通道 channels 剪枝/过滤器 filters 剪枝\n- `dim = 1`（Neurons）: 二维卷积核 kernel 剪枝，即与输入通道相连接的 kernel\n\n#### 2.2.3，局部结构化剪枝示例代码\n\n在写示例代码之前，我们先需要理解 `Conv2d` 函数参数、卷积核 shape、轴以及张量的关系。\n\n首先，Conv2d 函数原型如下;\n\n```py\nclass torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)\n```\n\n而 pytorch 中常规卷积的卷积核权重 `shape` 都为（`C_out, C_in, kernel_height, kernel_width`），所以在代码中卷积层权重 `shape` 为 `[3, 2, 3, 3]`，dim = 0 对应的是 shape [3, 2, 3, 3] 中的 `3`。这里我们 dim 设定了哪个轴，那自然剪枝之后权重张量对应的轴机会发生变换。\n\n![dim](../images/pruning_code/dim.png)\n\n理解了前面的关键概念，下面就可以实际使用了，`dim=0` 的示例如下所示。\n\n```python\nconv = torch.nn.Conv2d(2, 3, 3)\nnorm1 = torch.norm(conv.weight, p=1, dim=[1,2,3])\nprint(norm1)\n\"\"\"\ntensor([1.9384, 2.3780, 1.8638], grad_fn=<NormBackward1>)\n\"\"\"\nprune.ln_structured(conv, name=\"weight\", amount=1, n=2, dim=0)\nprint(conv.weight)\n\"\"\"\ntensor([[[[-0.0005,  0.1039,  0.0306],\n          [ 0.1233,  0.1517,  0.0628],\n          [ 0.1075, -0.0606,  0.1140]],\n\n         [[ 0.2263, -0.0199,  0.1275],\n          [-0.0455, -0.0639, -0.2153],\n          [ 0.1587, -0.1928,  0.1338]]],\n\n\n        [[[-0.2023,  0.0012,  0.1617],\n          [-0.1089,  0.2102, -0.2222],\n          [ 0.0645, -0.2333, -0.1211]],\n\n         [[ 0.2138, -0.0325,  0.0246],\n          [-0.0507,  0.1812, -0.2268],\n          [-0.1902,  0.0798,  0.0531]]],\n\n\n        [[[ 0.0000, -0.0000, -0.0000],\n          [ 0.0000, -0.0000, -0.0000],\n          [ 0.0000, -0.0000,  0.0000]],\n\n         [[ 0.0000,  0.0000,  0.0000],\n          [-0.0000,  0.0000,  0.0000],\n          [-0.0000, -0.0000, -0.0000]]]], grad_fn=<MulBackward0>)\n\"\"\"\n```\n\n从运行结果可以明显看出，卷积层参数的最后一个通道参数张量被移除了（为 `0` 张量），其解释参见下图。\n\n![dim_understand](../images/pruning_code/dim_understand.png)\n\n`dim = 1` 的情况：\n\n```python\nconv = torch.nn.Conv2d(2, 3, 3)\nnorm1 = torch.norm(conv.weight, p=1, dim=[0, 2,3])\nprint(norm1)\n\"\"\"\ntensor([3.1487, 3.9088], grad_fn=<NormBackward1>)\n\"\"\"\nprune.ln_structured(conv, name=\"weight\", amount=1, n=2, dim=1)\nprint(conv.weight)\n\"\"\"\ntensor([[[[ 0.0000, -0.0000, -0.0000],\n          [-0.0000,  0.0000,  0.0000],\n          [-0.0000,  0.0000, -0.0000]],\n\n         [[-0.2140,  0.1038,  0.1660],\n          [ 0.1265, -0.1650, -0.2183],\n          [-0.0680,  0.2280,  0.2128]]],\n\n\n        [[[-0.0000,  0.0000,  0.0000],\n          [ 0.0000,  0.0000, -0.0000],\n          [-0.0000, -0.0000, -0.0000]],\n\n         [[-0.2087,  0.1275,  0.0228],\n          [-0.1888, -0.1345,  0.1826],\n          [-0.2312, -0.1456, -0.1085]]],\n\n\n        [[[-0.0000,  0.0000,  0.0000],\n          [ 0.0000, -0.0000,  0.0000],\n          [ 0.0000, -0.0000,  0.0000]],\n\n         [[-0.0891,  0.0946, -0.1724],\n          [-0.2068,  0.0823,  0.0272],\n          [-0.2256, -0.1260, -0.0323]]]], grad_fn=<MulBackward0>)\n\"\"\"\n```\n\n 很明显，对于 `dim=1`的维度，其第一个张量的 L2 范数更小，所以shape 为 [2, 3, 3] 的张量中，第一个 [3, 3] 张量参数会被移除（即张量为 0 矩阵） 。\n\n### 2.3，全局非结构化剪枝\n\n前文的 local 剪枝的对象是特定网络层，而 global 剪枝是将模型看作一个整体去移除指定比例（数量）的参数，同时 global 剪枝结果会导致模型中每层的稀疏比例是不一样的。\n\n全局非结构化剪枝函数原型如下：\n\n```python\n# v1.4.0 版本\ndef global_unstructured(parameters, pruning_method, **kwargs)\n# v2.0.0-rc2版本\ndef global_unstructured(parameters, pruning_method, importance_scores=None, **kwargs):\n```\n\n1，**函数功能**：\n\n随机选择全局所有参数（包括权重和偏置）的一部分进行剪枝，而不管它们属于哪个层。\n\n2，**参数定义**：\n\n- `parameters`（(Iterable of (module, name) tuples)）: 修剪模型的参数列表，列表中的元素是  (module, name)。\n- `pruning_method`（function）: 目前好像官方只支持 pruning_method=prune.L1Unstuctured，另外也可以是自己实现的非结构化剪枝方法函数。\n- `importance_scores`: 表示每个参数的重要性得分，如果为 None，则使用默认得分。\n- `**kwargs`: 表示传递给特定剪枝方法的额外参数。比如 `amount` 指定要剪枝的数量。\n\n3，`global_unstructured` 函数的示例代码如下所示。\n\n```python\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nclass LeNet(nn.Module):\n    def __init__(self):\n        super(LeNet, self).__init__()\n        # 1 input image channel, 6 output channels, 3x3 square conv kernel\n        self.conv1 = nn.Conv2d(1, 6, 3)\n        self.conv2 = nn.Conv2d(6, 16, 3)\n        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5x5 image dimension\n        self.fc2 = nn.Linear(120, 84)\n        self.fc3 = nn.Linear(84, 10)\n\n    def forward(self, x):\n        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))\n        x = F.max_pool2d(F.relu(self.conv2(x)), 2)\n        x = x.view(-1, int(x.nelement() / x.shape[0]))\n        x = F.relu(self.fc1(x))\n        x = F.relu(self.fc2(x))\n        x = self.fc3(x)\n        return x\n\nmodel = LeNet().to(device=device)\n\nmodel = LeNet()\n\nparameters_to_prune = (\n    (model.conv1, 'weight'),\n    (model.conv2, 'weight'),\n    (model.fc1, 'weight'),\n    (model.fc2, 'weight'),\n    (model.fc3, 'weight'),\n)\n\nprune.global_unstructured(\n    parameters_to_prune,\n    pruning_method=prune.L1Unstructured,\n    amount=0.2,\n)\n# 计算卷积层和整个模型的稀疏度\n# 其实调用的是 Tensor.numel 内内函数，返回输入张量中元素的总数\nprint(\n    \"Sparsity in conv1.weight: {:.2f}%\".format(\n        100. * float(torch.sum(model.conv1.weight == 0))\n        / float(model.conv1.weight.nelement())\n    )\n)\nprint(\n    \"Global sparsity: {:.2f}%\".format(\n        100. * float(\n            torch.sum(model.conv1.weight == 0)\n            + torch.sum(model.conv2.weight == 0)\n            + torch.sum(model.fc1.weight == 0)\n            + torch.sum(model.fc2.weight == 0)\n            + torch.sum(model.fc3.weight == 0)\n        )\n        / float(\n            model.conv1.weight.nelement()\n            + model.conv2.weight.nelement()\n            + model.fc1.weight.nelement()\n            + model.fc2.weight.nelement()\n            + model.fc3.weight.nelement()\n        )\n    )\n)\n# 程序运行结果\n\"\"\"\nSparsity in conv1.weight: 3.70%\nGlobal sparsity: 20.00%\n\"\"\"\n```\n\n运行结果表明，虽然模型整体（全局）的稀疏度是 `20%`，但每个网络层的稀疏度不一定是 20%。\n\n## 三，总结\n\n另外，pytorch 框架还提供了一些帮助函数:\n\n1. torch.nn.utils.prune.is_pruned(module): 判断模块 是否被剪枝。\n2. torch.nn.utils.prune.remove(module, name)： 用于将指定模块中指定参数上的**剪枝操作移除**，从而恢复该参数的原始形状和数值。\n\n虽然 PyTorch 提供了内置剪枝 `API` ，也支持了一些非结构化和结构化剪枝方法，但是 `API` 比较混乱，对应文档描述也不清晰，所以后面我还会结合微软的开源 `nni` 工具来实现模型剪枝功能。\n\n更多剪枝方法实践，可以参考这个 `github` 仓库：[Model-Compression](https://github.com/tangchen2/Model-Compression)。\n## 参考资料\n\n1. [How to Prune Neural Networks with PyTorch](https://towardsdatascience.com/how-to-prune-neural-networks-with-pytorch-ebef60316b91)\n2. [PRUNING TUTORIAL](https://pytorch.org/tutorials/intermediate/pruning_tutorial.html)\n3. [PyTorch Pruning](https://leimao.github.io/blog/PyTorch-Pruning/)"
  },
  {
    "path": "5-model_compression/模型压缩-剪枝算法详解.md",
    "content": "- [一，前言](#一前言)\n  - [1.1，模型剪枝定义](#11模型剪枝定义)\n- [二，深度神经网络的稀疏性](#二深度神经网络的稀疏性)\n  - [2.1，权重稀疏](#21权重稀疏)\n  - [2.2，激活稀疏](#22激活稀疏)\n  - [2.3，梯度稀疏](#23梯度稀疏)\n  - [2.4，小结](#24小结)\n- [三，结构化稀疏](#三结构化稀疏)\n  - [3.1，结构化稀疏分类](#31结构化稀疏分类)\n    - [3.1.1，Channel/Filter 剪枝](#311channelfilter-剪枝)\n    - [3.1.2， 阶段级别剪枝](#312-阶段级别剪枝)\n  - [3.2，结构化稀疏与非结构化稀疏比较](#32结构化稀疏与非结构化稀疏比较)\n- [参考资料](#参考资料)\n\n## 一，前言\n\n学术界的 SOTA 模型在落地部署到工业界应用到过程中，通常是要面临着低延迟（`Latency`）、高吞吐（`Throughpout`）、高效率（`Efficiency`）挑战的。而模型压缩算法可以将一个庞大而复杂的预训练模型转化为一个精简的小模型，从而减少对硬件的存储、带宽和计算需求，以达到加速模型推理和落地的目的。\n\n近年来主流的模型压缩方法包括：**数值量化（Data Quantization，也叫模型量化）**，**模型稀疏化（Model sparsification，也叫模型剪枝 Model Pruning）**，**知识蒸馏（Knowledge Distillation）**， **轻量化网络设计（Lightweight Network Design）和 张量分解（Tensor Decomposition）**。\n\n其中模型剪枝是一种应用非常广的模型压缩方法，其可以**直接**减少模型中的参数量。本文会对模型剪枝的定义、发展历程、分类以及算法原理进行详细的介绍。\n\n### 1.1，模型剪枝定义\n\n模型剪枝（`Pruning`）也叫模型稀疏化，不同于模型量化对每一个权重参数进行压缩，稀疏化方法是尝试直接“删除”部分权重参数。模型剪枝的原理是通过剔除模型中 “不重要” 的权重，使得模型减少参数量和计算量，同时尽量保证模型的精度不受影响。\n\n## 二，深度神经网络的稀疏性\n\n生物研究发现人脑是高度稀疏的。比如 2016 年早期经典的剪枝论文[1]就曾提到，生理学上发现对于哺乳动物，婴儿期产生许多的突触连接，在后续的成长过程中，不怎么用的那些突触就会退化消失。结合深度神经网络是模仿人类大脑结构，和该生理学现象，我们可以认为深度神经网络是存在稀疏性的[1]。\n\n根据深度学习模型中可以被稀疏化的对象，深度神经网络中的稀疏性主要包括**权重稀疏**，**激活稀疏**和**梯度稀疏**。\n\n### 2.1，权重稀疏\n\n在大多数神经网络中，通过对网络层（卷积层或者全连接层）对权重数值进行直方图统计，可以发现，权重（训练前/训练后）的数值分布很像正太分布（或者是多正太分布的混合），且越接近于 0，权重越多，这就是**权重稀疏**现象。\n\n论文[1]认为，权重数值的绝对值大小可以看做重要性的一种度量，权重数值越大对模型输出贡献也越大，反正则不重要，删去后模型精度的影响应该也比较小。\n\n![权重参数重要性](../images/pruning/weights_important.png)\n\n当然，权重剪枝（**Weight Pruning**）虽然影响较小但不等于没有影响，且**不同类型、不同顺序的网络层，在权重剪枝后影响也各不相同**。论文[1]在 AlexNet 的 CONV 层和 FC 层的做了**剪枝敏感性**实验，结果如下图所示。\n\n![pruning sensitivity](../images/pruning/pruning_sensitivity.png)\n\n从图中实验结果可以看出，**卷积层的剪枝敏感性大于全连接层，且第一层卷积层最为敏感**。论文作者推测这是因为全连接层本身参数冗余性更大，第一层卷积层的输入只有 3 个通道所以比起他卷积层冗余性更少。\n\n即使是移除绝对值接近于 0 的权重也会带来推理精度的损失，因此为了恢复模型精度，通常在剪枝之后需要再训练模型。典型的模型剪枝三段式工作 `pipeline` 流程[1]和剪枝前后网络连接变化如下图所示。\n\n![classic pruning pipeline](../images/pruning/classic_pruning_pipeline.png)\n> 剪枝算法常用的是三段式工作 pipeline: 训练、剪枝、微调。\n\n上述算法步骤中，其中重点问题有两个，一个是如何评估连接权重的重要性，另一个是如何在重训练中恢复模型精度。对于**评估连接权重的重要性**，有两个典型的方法，一是基于神经元连接权重**数值幅度**的方法[1]，这种方法原理简单；二是使用目标函数对参数求二阶导数表示参数的贡献度[10]。\n\n> 基于神经元连接权重幅度的厕率好像在20世纪90年代就被提出来了，知识在韩松论文中[1]又被应用了。\n\n剪枝`Three-Step Training Pipeline` 中三个阶段权重数值分布如下图所示。微调之后的模型权重分布将部分地恢复正态分布的特性。\n\n![weight gaussian distribution](../images/pruning/weight_gaussian.png)\n> 深度网络中存在权重稀疏性:（a）剪枝前的权重分布；（b）剪除0值附近权值后的权重分布；（c）网络微调后的权重分布从单峰变成了双峰。\n\n值得注意的是，韩松提出的权重稀疏方法是细粒度稀疏，去只能在专用硬件上-EIE实现加速效果，是对硬件不友好的稀疏方法，因为其稀疏后得到的权重矩阵是**高度非规则的矩阵**，如下图所示。\n\n![高度非规则的矩阵形状](../images/pruning/irregular_matrix.png)\n\n### 2.2，激活稀疏\n\n早期的神经网络模型-早期的神经网络模型——多层感知机（MLP）中，多采用Sigmoid函数作为激活单元。但是其复杂的计算公式会导致模型训练过慢，且随着网络层数的加深，Sigmoid 函数引起的梯度消失和梯度爆炸问题严重影响了反向传播算法的实用性。为解决上述问题，Hinton 等人于 2010 年在论文中[2]提出了 `ReLU` 激活函数，并在 `AlexNet`模型[3]中第一次得到了实践。\n\n后续伴随着 `BN` 层算子的提出，“2D卷积-BN层-ReLU激活函数”三个算子串联而成的基本单元就构成了后来 CNN 模型的基础组件，如下述 `Pytorch` 代码片段所示：\n\n> 早期是 “2D卷积-ReLU激活函数-池化” 这样串接的组件。\n\n```python\n# cbr 组件示例代码\ndef convbn_relu(in_planes, out_planes, kernel_size, stride, pad, dilation):\n    return nn.Sequential(\n        nn.Conv2d(in_planes, out_planes, \n                  kernel_size=kernel_size, stride=stride, \n                  padding=dilation if dilation > 1 else pad, \n                  dilation=dilation, bias=False),\n        nn.BatchNorm2d(out_planes),\n        nn.ReLU(inplace=True)\n        )\n```\n\nReLU 激活函数的定义为：\n\n$$ReLU(x) = max(0, x)$$\n\n该函数使得负半轴的输入都产生 0 值的输出，这可以认为激活函数给网络带了另一种类型的稀疏性；另外 `max_pooling` 池化操作也会产生类似稀疏的效果。即无论网络接收到什么输入，大型网络中很大一部分神经元的输出大多为零。激活和池化稀疏效果如下图所示。\n\n![神经网络中的激活稀疏](../images/pruning/activation_sparse.png)\n> 即 ReLU 激活层和池化层输出特征图矩阵是稀疏的。\n\n受以上现象启发，论文[4]经过了一些简单的统计，发现无论输入什么样图像数据，CNN 中的许多神经元都具有非常低的激活。作者认为**零神经元**很可能是**冗余的**（`redundant`），可以在不影响网络整体精度的情况下将其移除。 因为它们的存在只会增加过度拟合的机会和优化难度，这两者都对网络有害。\n\n> 由于神经网络具有乘法-加法-激活计算过程，因此其输出大部分为零的神经元对后续层的输出以及最终结果的贡献很小。\n\n由此，提出了一种**神经元剪枝**算法。首先，定义了 `APoZ` （Average Percentage of Zeros）指标来衡量经过 ReLU  映射后神经元零激活的百分比。假设 $O_c^{(i)}$表示网络第 $i$ 层中第 $c$ 个通道（特征图），那么第 $i$ 层中第 $c$ 个的滤波器（论文中用神经元 neuron）的 APoZ 定义如下:\n\n$$APoZ^{(i)}_c = APoZ(O_c^{(i)}) = \\frac{\\sum_k^N \\sum_j^M f(O^{(i)}_{c,j}(k=0))}{N \\times M}$$\n\n这里，$f\\left( \\cdot \\right)$ 对真的表达式输出 1，反之输出 0，$M$ 表示  $O_c^{(i)}$ 输出特征图的大小，$N$ 表示用于验证的图像样本个数。由于每个特征图均来自一个滤波器（神经元）的卷积及激活映射结果，因此上式衡量了每个神经元的重要性。\n\n下图给出了在 VGG-16 网络中，利用 50,000 张 ImageNet 图像样本计算得到的 CONV5-3 层的 512 个和 FC6 层的 4096 个 APoZ 指标分布图。\n\n> 这里更高是指更接近于模型输出侧的网络层。\n\n![APoZ_distribution](../images/pruning/APoZ_distribution.png)\n\n可以看出 CONV5-3 层的大多数神经元的该项指标都分布在 93%附近。实际上，VGG-16 模型中共有 631 个神经元的 APoZ 值超过90%。激活函数的引入反映出 VGG  网络存在着大量的稀疏与冗余性，且大部分冗余都发生在更高的卷积层和全连接层。\n\n**激活稀疏的工作流程**和稀疏前后网络连接变化如下图所示。\n\n![activation_sparsification_pipeline](../images/pruning/activation_sparsification_pipeline.png)\n\n工作流程沿用韩松论文[1]提出的 Three-Step Training Pipeline，算法步骤如下所示:\n\n1. 首先，网络在常规过程下进行训练，每层中的神经元数量根据经验设置。 接下来，我们在大型验证数据集上运行网络以获得每个神经元的 APoZ。\n2. 根据特定标准修剪具有高 APoZ 的神经元。 当一个神经元被修剪时，与神经元的连接被相应地移除（参见上图右边红色框）。\n3. 神经元修剪后，修剪后的网络使用修剪前的权重进行初始化。 修剪后的网络表现出一定程度的性能下降。因此，在最后一步中，我们**重新训练**网络以加强剩余的神经元以增强修剪后网络的性能。\n\n**总结**：和权重剪枝的代表作[1]随机权重剪枝方法（神经元和连接都剪枝）相比，激活剪枝的代表作[4]，其剪枝的直接对象是神经元（neuron），即随机的将一些神经元的输出置零，属于结构化稀疏。\n\n### 2.3，梯度稀疏\n\n大模型（如 BERT）由于参数量庞大，单台主机难以满足其训练时的计算资源需求，往往需要借助分布式训练的方式在多台节点（Worker）上协作完成。采用分布式随机梯度下降（Distributed SGD）算法可以允许 $N$ 台节点共同完成梯度更新的后向传播训练任务。其中每台主机均保存一份完整的参数拷贝，并负责其中 $\\frac{1}{N}$ 参数的更新计算任务。按照一定时间间隔，节点在网络上发布自身更新的梯度，并获取其他 $N-1$ 台节点发布的梯度计算结果，从而更新本地的参数拷贝。\n\n随着参与训练任务节点数目的增多，网络上传输的模型梯度数据量也急剧增加，网络通信所占据的资源开销将逐渐超过梯度计算本身所消耗的资源，从而严重影响大规模分布式训练的效率。另外，大多数深度神经网络模型参数的梯度其实也是高度稀疏的，有研究[5]表明在分布式 `SGD` 算法中，99.9% 的梯度交换都是**冗余**的。例如下图显示了在 AlexNet 的训练早期，各层参数梯度的幅值还是较高的。但随着训练周期的增加，**参数梯度的稀疏度显著增大**。\n\n> AlexNet 模型的训练是采用分布式训练。深度神经网络训练中的各层梯度值存在高度稀疏特性。\n\n![Alex_grad_sparse](../images/pruning/Alex_grad_sparse.png)\n\n因为梯度交换成本很高，由此导致了网络带宽成为了分布式训练的瓶颈，为了克服分布式训练中的通信瓶颈，梯度稀疏（`Gradient Sparsification`）得到了广泛的研究，其实现的途径包括：\n\n1. **选择固定比例的正负梯度更新**：在网络上传输根据一定比例选出的一部分正、负梯度更新值。[Dryden 等人2016年的论文](https://dl.acm.org/doi/abs/10.5555/3018874.3018875)。\n2. **预设阈值**：在网络上仅仅传输那些绝对值幅度超过预设阈值的梯度。[Heafield (2017)论文](https://arxiv.org/abs/1704.05021)。\n\n**深度梯度压缩（DGC）**[5]，在梯度稀疏化基础上采用动量修正、本地梯度剪裁、动量因子遮蔽和 warm-up训练 4 种方法来做梯度压缩，从而减少分布式训练中的通信带宽。其算法效果如下图所示。\n\n![deep_gradient_sparse](../images/pruning/deep_gradient_sparse.png)\n\n### 2.4，小结\n\n虽然神经网络稀疏化目前在学术界研究和工业界落地已经取得了写进展，但是目前并没有一个完全确定的知识体系框架，许多以前 paper 上的结论是可能被后续新论文打破和重建的。以下是对主流权重剪枝、激活剪枝和梯度剪枝的总结:\n\n1. 早期的权重剪枝是**非结构化**的（细粒度稀疏）其对并行计算硬件-GPU支持并不友好，甚至可能完全没有效果，其加速效果需要在专用加速器硬件（一般是 **ASIC**）上实现，比如韩松提出的 EIE 加速硬件[6]。\n2. 更高层的网络冗余性更大，且卷积层的冗余性比全连接层的冗余性更少。所以 ResNet、MobileNet 等网络的剪枝难度大于 VGG、AlexNet 等。\n3. 神经元剪枝相比权重剪枝更易损失模型精度，训练阶段的梯度则拥有最多的稀疏度。\n4. 典型的模型剪枝三段式工作 `pipeline` 流程并不一定是准确的，最新的研究表明，对于随机初始化网络先进行剪枝操作再进行训练，有可能会比剪枝预训练网络获得更高的稀疏度和精度。此需要更多研究。\n\n神经网络的权重、激活和梯度稀疏性总结如下表所示:\n\n![model_sparsification_summary](../images/pruning/model_sparsification_summary.png)\n\n## 三，结构化稀疏\n\n早期提出的连接权重稀疏化[1]方法是非结构化稀疏（即细粒度稀疏，也叫非结构化剪枝），其直接将模型大小压缩10倍以上，理论上也可以减少10倍的计算量。但是，细粒度的剪枝带来的计算特征上的“不规则”，对计算设备中的数据访问和大规模并行计算非常不友好，尤其是对 `GPU`硬件！\n\n>  论文[1]作者提出了专用加速器硬件 `EIE` 去支持他的细粒度权重剪枝算法。\n\n因为，“非结构化稀疏”（Unstructured Sparsity）主要通过对权重矩阵中的单个或整行、整列的权重值进行修剪。修剪后的新权重矩阵会变成稀疏矩阵（被修剪的值会设置为 0）。因而除非硬件平台和计算库能够支持高效的稀疏矩阵计算，否则剪枝后的模型是无法获得真正的性能提升的！\n\n由此，许多研究开始探索通过给神经网络剪枝添加一个“规则”的约束-**结构化剪枝**（Structured pruning），使得剪枝后的稀疏模式更加适合并行硬件计算。 “结构化剪枝”的基本修剪单元是**卷积核**或权重矩阵的一个或多个Channel。由于结构化剪枝没有改变权重矩阵本身的稀疏程度，现有的计算平台和框架都可以实现很好的支持。\n\n 这种引入了“规则”的结构化约束的稀疏模式通常被称为结构化稀疏（Structured Sparsity），在很多文献中也被称之为粗粒度稀疏（Coarse-grained Sparsity）或块稀疏（Block Sparsity），结构化和非结构化稀疏针对的都是权重参数。\n\n### 3.1，结构化稀疏分类\n\n结构化剪枝又可进一步细分：可以是 channel/filter-wise，也可以是 shape-wise 等。\n\n> 过滤器 filter，也叫滤波器，相当于 3 维对卷积核。输出的 `feature map` 的数量/通道数等于滤波器数量。\n\n#### 3.1.1，Channel/Filter 剪枝\n\nchannel 剪枝的工作是最多的，channel 剪枝和 filter 剪枝其实意义是一样的，一个过滤器移除了，对应输出 feature map 的一个通道自然也被移除，反之一样。\n\nfilter (channel) pruning (FP) 属于粗粒度剪枝（或者叫结构化剪枝 Structured Pruning），基于 FP 的方法修剪的是过滤器或者卷积层中的通道，而不是对个别权重，其原始的卷积结构不改变，所以剪枝后的模型不需要专门的算法和硬件就能够加速运行。\n\n`CNN` 模型中通道剪枝的核心在于如何减少中间特征的数量，其中一个经典思路是基于**重要性因子**，即评估一个通道的有效性，再配合约束一些通道使得模型结构本身具有稀疏性，从而基于此进行剪枝。\n\n> 基于重要性因子的方法进行通道剪枝，和前面非结构化剪枝中的基于权重幅度的方法来进行连接剪枝类似，都有点主观性太强。\n\n论文[Learning Efficient Convolutional Networks through Network Slimming](https://arxiv.org/pdf/1708.06519.pdf)[7] 认为 conv-layer 的每个channel 的重要程度可以和 bn 层关联起来，如果某个 channel 后的 `bn` 层中对应的 scaling factor 足够小，就说明该 channel 的重要程度低，可以被忽略。如下图中橙色的两个通道被剪枝。\n\n![channel_pruning](../images/pruning/channel_pruning.png)\n\n`BN` 层的计算公式如下：\n\n$$\n\\begin{aligned} \n\\mu_B &= \\frac{1}{m}\\sum_1^m x_i \\\\\n\\sigma^2_B &= \\frac{1}{m} \\sum_1^m (x_i-\\mu_B)^2 \\\\\nn_i &= \\frac{x_i-\\mu_B}{\\sqrt{\\sigma^2_B + \\epsilon}} \\\\\nz_i &= \\gamma n_i + \\beta = \\frac{\\gamma}{\\sqrt{\\sigma^2_B + \\epsilon}}x_i + (\\beta - \\frac{\\gamma\\mu_{B}}{\\sqrt{\\sigma^2_B + \\epsilon}})\n\\end{aligned}\n$$\n\n其中，`bn` 层中的 $\\gamma$ 参数被作为 `channel-level 剪枝` 所需的缩放因子（`scaling factor`）。\n\n#### 3.1.2， 阶段级别剪枝\n\n滤波器级别的剪枝只能作用于残差结构块内部的卷积层，CURL[9]中指出只进行滤波器级别的剪枝会导致模型形成一个沙漏状、两头宽中间窄的结构，这样的结构会影响参数量和计算量。在这种情况下，阶段级别的剪枝能弥补滤波器级别剪枝的不足。\n\n一个阶段中的残差结构块是紧密联系在一起的，如下图所示。\n\n![structured_way_compare](../images/pruning/structured_way_compare.png)\n\n当一个阶段的输出特征发生变化时（一些特征被抛弃），其对应的每个残差结构的输入特征和输出特征都要发生相应的变化，所以整个阶段中，每个残差结构的第一个卷积层的输入通道数，以及最后一个卷积层的输出通道数都要发生相同的变化。由于这样的影响只限定在当前的阶段，不会影响之前和之后的阶段，因此我们称这个剪枝过程为阶段级别的剪枝（stage-level pruning）。 \n\n阶段级别的剪枝加上滤波器级别的剪枝能够使网络的形状更均匀，而避免出现沙漏状的网络结构。此外，阶段级别的剪枝能够剪除更多的网络参数，这给网络进一步压缩提供了支持。\n\n### 3.2，结构化稀疏与非结构化稀疏比较\n\n与非结构化剪枝相比，结构化剪枝通常通常会牺牲模型的准确率和压缩比。结构化稀疏对非零权值的位置进行了限制，在剪枝过程中会将一些数值较大的权值剪枝，从而影响模型准确率。 “非规则”的剪枝则契合了神经网络模型中不同大小权值的**随机分布**，这对深度学习模型的准确度至关重要。展开来讲就是：\n\n1. 非结构化稀疏具有更高的模型压缩率和准确性，在通用硬件上的加速效果不好。因为其计算特征上的“不规则”，导致需要特定硬件支持才能实现加速效果。\n2. 结构化稀疏虽然牺牲了模型压缩率或准确率，但在通用硬件上的加速效果好，所以其被广泛应用。因为结构化稀疏使得权值矩阵更规则更加结构化，更利于硬件加速。\n\n![Unstructured_structured_sparsity](../images/pruning/Unstructured_structured_sparsity.png)\n\n综上所述，深度神经网络的权值稀疏应该在**模型有效性和计算高效性之间做权衡**。\n\n目前，有一种趋势是在软硬件上都支持**稀疏张量**，因此未来非结构化剪枝可能会变得更流行。\n\n## 参考资料\n\n- [1]:[Learning both Weights and Connections for Efficient](https://arxiv.org/pdf/1506.02626.pdf)\n- [2]:[Rectified Linear Units Improve Restricted Boltzmann Machines](https://www.cs.toronto.edu/~hinton/absps/reluICML.pdf)\n- [3]:[ImageNet Classification with Deep Convolutional](https://proceedings.neurips.cc/paper/2012/file/c399862d3b9d6b76c8436e924a68c45b-Paper.pdf)\n- [4]:[Network Trimming: A Data-Driven Neuron Pruning Approach towards Efficient Deep Architectures](https://arxiv.org/pdf/1607.03250.pdf)\n- [5]:[Deep Gradient Compression: Reducing the Communication Bandwidth for Distributed Training](https://arxiv.org/abs/1712.01887)\n- [6]:[韩松博士毕业论文-EFFICIENT METHODS AND HARDWARE FOR DEEP LEARNING](https://stacks.stanford.edu/file/druid:qf934gh3708/EFFICIENT%20METHODS%20AND%20HARDWARE%20FOR%20DEEP%20LEARNING-augmented.pdf)\n- [7]:[Learning Efficient Convolutional Networks through Network Slimming](https://arxiv.org/pdf/1708.06519.pdf)\n- [8]: [第1章 结构化剪枝综述](https://cs.nju.edu.cn/wujx/paper/Pruning_Survey_MLA21.pdf)\n- [9]: [Neural network pruning with residual-connections and limited-data](https://arxiv.org/pdf/1911.08114.pdf)\n- [10]: [Optimal Brain Damage](https://proceedings.neurips.cc/paper/1989/file/6c9882bbac1c7093bd25041881277658-Paper.pdf)\n- [11]: [AI-System: 11.3 模型压缩与硬件加速](https://github.com/microsoft/AI-System/blob/main/Textbook/%E7%AC%AC11%E7%AB%A0-%E6%A8%A1%E5%9E%8B%E5%8E%8B%E7%BC%A9%E4%B8%8E%E5%8A%A0%E9%80%9F/11.3-%E6%A8%A1%E5%9E%8B%E5%8E%8B%E7%BC%A9%E4%B8%8E%E7%A1%AC%E4%BB%B6%E5%8A%A0%E9%80%9F.md)"
  },
  {
    "path": "5-model_compression/模型压缩-知识蒸馏详解.md",
    "content": ""
  },
  {
    "path": "5-model_compression/模型压缩-神经网络量化基础.md",
    "content": "---\nlayout: post\ntitle: 模型压缩-神经网络量化基础\ndate: 2023-03-05 19:00:00\nsummary: 总结线性量化优点、原理、方法和实战基础。\ncategories: Model_Compression\n---\n\n- [一 模型量化概述](#一-模型量化概述)\n  - [1.1 模型量化优点](#11-模型量化优点)\n  - [1.2 模型量化的方案](#12-模型量化的方案)\n    - [1.2.1 PTQ 理解](#121-ptq-理解)\n  - [1.3 量化的分类](#13-量化的分类)\n    - [1.3.1 线性量化概述](#131-线性量化概述)\n- [二 量化算术](#二-量化算术)\n  - [2.1 定点和浮点](#21-定点和浮点)\n  - [2.2 量化浮点](#22-量化浮点)\n  - [2.2.1 对称量化(均匀量化)](#221-对称量化均匀量化)\n  - [2.2.2 非对称量化](#222-非对称量化)\n  - [2.2，量化算术](#22量化算术)\n- [三，量化方法的改进](#三量化方法的改进)\n  - [3.1，浮点数动态范围选择](#31浮点数动态范围选择)\n  - [3.2，最大最小值（MinMax）](#32最大最小值minmax)\n  - [3.3，滑动平均最大最小值(MovingAverageMinMax)](#33滑动平均最大最小值movingaverageminmax)\n  - [3.4，KL 距离采样方法(Kullback–Leibler divergence)](#34kl-距离采样方法kullbackleibler-divergence)\n  - [3.5，总结](#35总结)\n- [参考资料](#参考资料)\n\n> 总结线性量化优点、原理、方法和实战基础，主要参考 [神经网络量化简介](https://jackwish.net/2019/neural-network-quantization-introduction-chn.html) 并加以自己的理解和总结，适合初学者阅读和自身复习用。\n\n## 一 模型量化概述\n\n### 1.1 模型量化优点\n\n模型量化指将权重为浮点数的 `FP32` 模型转换为整数的 `INT8/INT4` 模型，其中包括两个过程：FP32 的浮点模型转为 INT8，以及使用 INT8 权重进行推理。量化推理和低精度（Low precision）推理意义相同。\n\n- 低精度模型表示模型权重数值格式为 `FP16`（半精度浮点）或者 `INT8`（8位的定点整数），但是目前低精度往往就指代 `INT8`。\n- 常规精度模型则一般表示模型权重数值格式为 `FP32`（32位浮点，单精度）。\n- 混合精度（Mixed precision）则在模型中同时使用 `FP32` 和 `FP16` 的权重数值格式。 `FP16` 减少了一半的内存大小，但有些参数或操作符必须采用 `FP32` 格式才能保持准确度。\n\n模型量化有以下好处：\n\n> 参考[ TensorFlow 模型优化：模型量化-张益新](https://mp.weixin.qq.com/s/9QeVVESP3_rBZ6n_D96lwg)\n\n+ **减小模型大小**：如 `int8` 量化可减少 `75%` 的模型大小，`int8` 量化模型大小一般为 `32` 位浮点模型大小的 `1/4`：\n    + 减少存储空间：在端侧存储空间不足时更具备意义。\n    + 减少内存占用：更小的模型当然就意味着不需要更多的内存空间。\n    + 减少设备功耗：内存耗用少了推理速度快了自然减少了设备功耗；\n+ **加快推理速度**，访问一次 `32` 位浮点型可以访问四次 `int8` 整型，整型运算比浮点型运算更快；`CPU` 用 `int8` 计算的速度更快\n+ **某些硬件加速器如 DSP/NPU 只支持 int8**。比如有些微处理器属于 `8` 位的，低功耗运行浮点运算速度慢，需要进行 `8bit` 量化。\n\n总结：模型量化主要意义就是加快模型端侧的推理速度，并降低设备功耗和减少存储空间，\n\n工业界一般只使用 `INT8` 量化模型，如 `NCNN`、`TNN` 等移动端模型推理框架都支持模型的 `INT8` 量化和量化模型的推理功能。\n\n通常，可以根据 `FP32` 和 `INT8` 的转换策略对**量化模型推理方案**进行分类。一些框架简单地引入了 `Quantize` 和 `Dequantize` 层，当从卷积或全链接层送入或取出时，它将 `FP32` 转换为 `INT8` 或相反。在这种情况下，如下图的上半部分所示，模型本身和输入/输出采用 `FP32` 格式。深度学习推理框架加载模型时，重写网络以插入 `Quantize` 和 `Dequantize` 层，并将权重转换为 `INT8` 格式。\n> 注意，之所以要插入反量化层（`Dequantize`），是因为量化技术的早期，只有卷积算子支持量化，但实际网络中还包含其他算子，而其他算子又只支持 `FP32` 计算，因此需要把 INT8 转换成 FP32。但随着技术的迭代，后期估计会逐步改善乃至消除  `Dequantize` 操作，达成全网络的量化运行，而不是部分算子量化运行。\n\n<div align=\"center\">\n<img src=\"../images/quantization/reasoning_with_quantized_models.svg\" width=\"60%\" alt=\"量化模型的推理\">\n</div>\n\n> 图四：混合 FP32/INT8 和纯 INT8 推理。红色为 FP32，绿色为 INT8 或量化。\n\n其他一些框架将网络整体转换为 `INT8` 格式，因此在推理期间没有格式转换，如上图的下半部分。该方法要求算子（`Operator`）都支持量化，因为运算符之间的数据流是 `INT8`。对于尚未支持的那些，它可能会回落到 `Quantize/Dequantize` 方案。\n\n### 1.2 模型量化的方案\n\n在实践中将浮点模型转为量化模型的方法有以下三种方法：\n\n1. `data free`：不使用校准集，传统的方法直接将浮点参数转化成量化数，使用上非常简单，但是一般会带来很大的精度损失，但是高通最新的论文 `DFQ` 不使用校准集也得到了很高的精度。\n2. `calibration`：基于校准集方案，通过输入少量真实数据进行统计分析。很多芯片厂商都提供这样的功能，如 `tensorRT`、高通、海思、地平线、寒武纪\n3. `finetune`：基于训练 `finetune` 的方案，将量化误差在训练时仿真建模，调整权重使其更适合量化。好处是能带来更大的精度提升，缺点是要修改模型训练代码，开发周期较长。\n\n`TensorFlow` 框架按照量化阶段的不同，其模型量化功能分为以下两种：\n\n+ Post-training quantization `PTQ`（训练后量化、离线量化）；\n+ Quantization-aware training `QAT`（训练时量化，伪量化，在线量化）。\n\n#### 1.2.1 PTQ 理解\n\n`PTQ` `Post Training Quantization` 是训练后量化，也叫做离线量化，根据量化零点 $x_{zero}$ 是否为 `0`，训练后量化分为对称量化和非对称量化；根据数据通道顺序 `NHWC`(TensorFlow) 这一维度区分，训练后量化又分为逐层量化和逐通道量化。目前 `nvidia` 的 `TensorRT` 框架中使用了逐层量化的方法，每一层采用同一个阈值来进行量化。逐通道量化就是对每一层每个通道都有各自的阈值，对精度可以有一个很好的提升。\n\n### 1.3 量化的分类\n\n目前已知的加快推理速度概率较大的量化方法主要有：\n\n1. **二值化**，其可以用简单的位运算来同时计算大量的数。对比从 nvdia gpu 到 x86 平台，1bit 计算分别有 5 到128倍的理论性能提升。且其只会引入一个额外的量化操作，该操作可以享受到 SIMD（单指令多数据流）的加速收益。\n2. **线性量化**(最常见)，又可细分为非对称，对称和 `ristretto` 几种。在 `nvdia gpu`，`x86`、`arm` 和 部分 `AI` 芯片平台上，均支持 `8bit` 的计算，效率提升从 `1` 倍到 `16` 倍不等，其中 `tensor core` 甚至支持 `4bit`计算，这也是非常有潜力的方向。线性量化引入的额外量化/反量化计算都是标准的向量操作，因此也可以使用 `SIMD` 进行加速，带来的额外计算耗时不大。\n3. **对数量化**，一种比较特殊的量化方法。两个同底的幂指数进行相乘，那么等价于其指数相加，降低了计算强度。同时加法也被转变为索引计算。目前 `nvdia gpu`，`x86`、`arm` 三大平台上没有实现对数量化的加速库，但是目前已知海思 `351X` 系列芯片上使用了对数量化。\n\n#### 1.3.1 线性量化概述\n\n与非线性量化不同，线性量化采用均匀分布的聚类中心，原始浮点数据和量化后的整数存在一个简单的线性变换关系，因为卷积、全连接等网络层本身只是简单的线性计算，因此线性量化中可以直接用量化后的数据进行直接计算。\n\n根据量化前后浮点空间中的零的量化值是否依然是 0，可以将浮点数的线性量化分为两类-**对称量化** Symmetric Quantization 和**非对称量化** Asymmetric Quantization。\n\n<div align=\"center\">\n<img src=\"../images/quantization/symmetric_quantization2.png\" width=\"60%\" alt=\"对称量化喝非对称量化\">\n</div>\n\n## 二 量化算术\n\n**模型量化过程可以分为两部分：将模型从 FP32 转换为 INT8，以及使用 INT8 进行推理**。本节说明这两部分背后的算术原理。如果不了解基础算术原理，在考虑量化细节时通常会感到困惑。\n\n### 2.1 定点和浮点\n\n**定点和浮点都是数值的表示**（representation），它们区别在于，将整数（integer）部分和小数（fractional）部分分开的点，点在哪里。**定点保留特定位数整数和小数，而浮点保留特定位数的有效数字（significand）和指数（exponent）**。\n\n绝大多数现代的计算机系统采纳了**浮点数表示方式**，这种表达方式利用科学计数法来表达实数。即用一个尾数(Mantissa，尾数有时也称为有效数字，它实际上是有效数字的非正式说法)，一个基数(Base)，一个指数(Exponent)以及一个表示正负的符号来表达实数。具体组成如下：\n\n- 第一部分为 `sign` 符号位 $s$，占 1 bit，用来表示正负号；\n- 第二部分为 `exponent` 指数偏移值 $k$，占 8 bits，用来表示其是 2 的多少次幂；\n- 第三部分是 `fraction` 分数值（有效数字） $M$，占 23 bits，用来表示该浮点数的数值大小。\n\n基于上述表示，浮点数的值可以用以下公式计算：\n\n$$(-1)^s \\times M \\times 2^k$$\n\n值得注意是，上述公式隐藏了一些细节，如指数偏移值 $k$ 使用的时候需要加上一个固定的偏移值。\n\n比如 `123.45` 用十进制科学计数法可以表示为 $1.2345\\times 10^2$，其中 `1.2345` 为尾数，`10` 为基数，`2` 为指数。\n\n单精度浮点类型 `float` 占用 `32bit`，所以也称作 `FP32`；双精度浮点类型 `double` 占用 `64bit`。\n\n<div align=\"center\">\n<img src=\"../images/quantization/fixed_point_and_floating_point_formats_and_examples.jpg\" width=\"60%\" alt=\"定点和浮点的格式和示例\">\n</div>\n> 图五：定点和浮点的格式和示例。\n\n### 2.2 量化浮点\n\n`32-bit` 浮点数 FP32 和 `8-bit` 整数 INT8 的表示范围如下表所示：\n\n|数据类型|最小值|最大值|\n|------------|---------|----------|\n|`FP32`| -3.4e38|3.4e38|\n|`int8`|-128|128|\n|`uint8`|0|255|\n\n神经网络的推理由浮点运算构成。`FP32` 和 `INT8` 的值域是 $[(2−2^{23})×2^{127},(2^{23}−2)\\times 2^{127}]$ 和 $[−128,127]$，而取值数量大约分别为 $2^{32}$ 和 $2^8$ 。`FP32` 取值范围非常广，因此，将网络从 `FP32` 转换为 `INT8` 并不像数据类型转换截断那样简单。\n\n值的注意的是，一般神经网络层权重的值分布范围很窄，非常接近零。图八给出了 `MobileNetV1` 中十层（拥有最多值的层）的权重分布。\n\n<div align=\"center\">\n<img src=\"../images/quantization/weight_distribution_of_ten_layer_mobilenetv1.svg\" width=\"60%\" alt=\"十层 MobileNetV1 的权重分布\">\n</div>\n> 图八：十层 MobileNetV1 的权重分布。\n\n### 2.2.1 对称量化(均匀量化)\n\n对称量化的浮点值和 `8` 位定点值的映射关系如下图，从图中可以看出，对称量化就是将一个 `tensor` 中的 $\\left[-max(\\left\\| x \\right\\|),max(\\left\\| x \\right\\|)\\right]$ 内的 `FP32` 值分别映射到 `8 bit` 数据的 $[-128, 127]$ 的范围内，中间值按照线性关系进行映射，称这种映射关系是对称量化。可以看出，对称量化的浮点值和量化值范围都是相对于零对称的，所以叫对称量化。\n\n<div align=\"center\">\n<img src=\"../images/quantization/symmetric_quantization.png\" width=\"60%\" alt=\"对称量化\">\n</div>\n \n假设 $x_f$ 表示 FP32 权重 $x_{float}$， $x_q$ 表示量化的 INT8 权重值（整数）$x_{quantized}$，$x_s$ 是缩放因子 $x_{scale}$（映射因子、量化尺度（范围）、FP32 的缩放系数），`round` 为取整函数。对权权值和数据的量化可以归结为寻找 $scale$ 的过程，量化方法的改进本质上是选择最优 $scale$ 值的过程。\n\n$$x_f = x_s \\times x_q$$\n\n对称量化的一个最经典方法是**最大绝对值量化**，它对所有数据取绝对值操作，使得量化前浮点数取值范围变为 $\\left[0, max(\\left\\| x \\right\\|)\\right]$，量化后取值范围变为 $[0, 2^{N} - 1]$，对于 `INT8` 量化则是 $[0, 127]$，由此，缩放因子 `scale` 和量化值的计算公式如下:\n\n$$x_s = \\frac{max(\\left|x\\right|)}{127} \\\\\nx_q = round(\\frac{x_f}{x_s})$$\n\n### 2.2.2 非对称量化\n\n因为对称量化的缩放方法可能会将 FP32 零映射到 INT8 零，但我们不希望这种情况出现，于是出现了数字信号处理中的均一量化，即**非对称量化**。数学表达式如下所示，其中 $x_{zero}$ 表示量化零点（量化偏移），也的文章写作 $offset\\in [-128, 127]$。\n\n$$x_f = x_s \\times (x_q - x_{zero})$$\n\n大多数情况下量化是选用无符号整数，即 `UINT8` 的值域就为 $[0,255]$ ，这种情况，更能体现非对称量化的意义。非对称量化的浮点值和 `UINT8` 量化值的映射关系如下图：\n\n<div align=\"center\">\n<img src=\"../images/quantization/asymmetric_quantization.png\" width=\"60%\" alt=\"非对称量化\">\n</div>\n\n**权重参数的非对称量化算法可以分为两个步骤**：\n\n1. 通过在权重张量（Tensor）中找到 $min$ 和 $max$ 值从而确定缩放系数 $x_s$ 和零点偏移值 $x_{zero}$。\n2. 将权重张量的每个值从 FP32 转换为 INT8。\n  \n$$\\begin{align}\nx_f &\\in [x_f^{min}, x_f^{max}] \\\\\nx_s &= \\frac{x_f^{max} - x_f^{min}}{x_q^{max} - x_q^{min}} \\\\\nx_{zero} &= x_q^{max} - x_f^{max} \\div x_s \\\\\nx_q &= round(\\frac{x_f}{x_s}) + x_{zero}\n\\end{align}$$\n\n注意，当浮点运算结果不等于整数时，需要**额外的取整操作 `round`**。例如将 FP32 值域 [−1,1] 映射到 INT8 值域 [0,255]，有 $x_s=\\frac{2}{255}$，而 $x_{zero}= 255−\\frac{255}{2}≈127$。\n\n注意，量化过程中存在误差是不可避免的，就像数字信号处理中量化一样。**非对称算法一般能够较好地处理数据分布不均匀的情况**。\n\n总结：非对称量化可以看作是非对称量化 $x_{zero} = 0$ 的特例。\n\n### 2.2，量化算术\n\n量化的一个重要问题是**如何用量化算术表示非量化算术**，在支持量化模型推理的推理框架中，就是指量化 `kernel`（INT8 Kernel） 如何实现，用于替代常规神经网络的 FP32 计算。量化算术表示的原理和浮点乘法过程非常相似。\n> 量化 kernel 代码可能会涉及到反量化过程，也就是如何将 `INT8` 整数反量化成 `FP32` 的浮点数据。\n\n下面的等式 5-10 是量化（定点）乘法替代非量化（非定点）乘法的计算过程。\n\n$$\\begin{align}\nz_{float} & = x_f \\cdot y_{f} \\\\\nz_{s} \\cdot (z_q - z_{zero})\n& = (x_s \\cdot (x_q - x_{zero})) \\cdot\n(y_s \\cdot (y_q - y_{zero})) \\\\\nz_q - z_{zero}\n&= \\frac{x_s \\cdot y_s}{z_{s}} \\cdot\n(x_q - x_{zero}) \\cdot (y_q - y_{zero}) \\\\\nz_q\n&= \\frac{x_s \\cdot y_s}{z_{s}} \\cdot\n(x_q - x_{zero}) \\cdot (y_q - y_{zero}) + z_{zero} \\\\\n\\alpha &= \\frac{x_s \\cdot y_s}{z_{s}} \\\\\nz_q\n&= \\alpha \\cdot (x_q - x_{zero}) \\cdot\n(y_q - y_{zero}) + z_{zero} \\\\\n\\end{align}$$\n\n> 等式：量化乘法运算。\n\n对于给定神经网络，在完成 fp32 模型量化成 int8 模型之后，输入 $x$、权重 $y$ 和输出 $z$ 的缩放因子 $scale$ 肯定是已知的，因此等式 14 的 $\\alpha = \\frac{x_s y_s}{z_{s}}$ 也是已知且是 `FP32` 常数，在量化 kernel 运行之前提前计算好的。对于等式 `10` 可以应用的大多数情况，**$quantized$ 和 $zero\\_point$ 的变量 $(x,y)$ 都是 `INT8` 类型，$scale$ 变量是 `FP32`**。\n\n由此，可知除了 $\\alpha$ 和 $(x_q - x_{zero})\\cdot (y_q - y_{zero})$ 之间的乘法外，等式 10 中的其他运算都是量化运算。\n\n另外，两个 `INT8` 之间的算术运算会累加到 `INT16` 或 `INT32`，这时 `INT8` 的值域可能无法保存运算结果。例如，对于 $x_q=20$、$x_{zero} = 50$ 的情况，有 $(x_q − x_{zero}) = −30$ 超出 `INT8` 值范围 $[0,255]$。\n\n数据类型转换可能将 $\\alpha \\cdot (x_q - x_{zero}) \\cdot (y_q - y_{zero})$ 转换为 INT32 或 INT16，结合 $ + z_{zero}$ 一起可以确保计算结果几乎全部落入 INT8 值域 [0,255] 中。\n\n对于以上情况，在工程中，比如对于卷积算子的计算，`sum(x*y)` 的结果需要用 INT32 保存，同时，`b` 值一般也是 `INT32` 格式的，之后再 `requantize` (重新量化)成 `INT8`。\n\n除了量化乘法和加法外，常见的还有量化除法、减法、指数等数值计算，它们都有量化乘法的这些操作，也都有特定的方法分界为乘法和加法。采用这些方法，量化神经网络可以运行得到和原始网络一样有效的结果。\n\n## 三，量化方法的改进\n\n量化浮点部分中描述权重浮点量化方法是非常简单的。在深度学习框架的早期开发中，这种简单的方法能快速跑通 `INT8` 推理功能，然而采用这种方法的网络的预测准确度通常会出现明显的下降。\n\n虽然 FP32 权重的值域很窄，在这值域中数值点数量却很大。以上文的缩放为例，$[−1,1]$ 值域中 $2^{31}$（是的，基本上是总得可表示数值的一半）个 FP32 值被映射到 $256$ 个 INT8 值。\n\n+ 量化类型：（`SYMMETRIC`） 对称量化和 (`NON-SYMMETRIC`） 非对称量化；\n+ 量化算法：`MINMAX`、`KL` 散度、`ADMM`；\n+ 权重量化类型：`per-channel` `per-layer`；\n\n采用普通量化方法时，靠近零的浮点值在量化时没有精确地用定点值表示。因此，与原始网络相比，量化网络一般会有明显的精度损失。对于线性（均匀）量化，这个问题是不可避免的。\n\n同时值映射的精度是受由 $x_f^{min}$ 和 $x_f^{max}$ 得到的 $x_s$ 显著影响的。并且，如图十所示，权重中邻近 $x_f^{min}$ 和 $x_f^{max}$ 附近的值通常是可忽略的，其实就等同于**映射关系中浮点值的 `min` 和 `max` 值是可以通过算法选择的**。\n\n<div align=\"center\">\n<img src=\"../images/quantization/figure_10_adjusting_the_minimum_maximum_value_when_quantizing_floating_point_to_fixed_point.jpg\" width=\"60%\" alt=\"图十将浮点量化为定点时调整最小值-最大值\">\n</div>\n> 图十将浮点量化为定点时调整最小值-最大值。\n\n上图展示了可以调整 `min/max` 来选择一个值域，使得值域的值更准确地量化，而范围外的值则直接映射到定点的 min/max。例如，当从原始值范围 $[−1,1]$ 中选定$x_{min}^{float} = −0.9$ 和 $x_{max}^{float} = 0.8$ ，$[−0.9,0.8]$ 中的值将能更准确地映射到 $[0,255]$ 中，而 $[−1,−0.9]$ 和 $[0.8,1]$ 中的值分别映射为 $0$ 和 $255$。\n\n### 3.1，浮点数动态范围选择\n\n> 参考[干货：深度学习模型量化（低精度推理）大总结](https://mp.weixin.qq.com/s/LR3Z2rlkxdl-1KnsU734VQ)。\n\n通过前文对量化算数的理解和上面两种量化算法的介绍我们不难发现，为了计算 `scale` 和 `zero_point`，我们需要知道 `FP32 weight/activation` 的实际动态范围。对于推理过程来说，`weight` 是一个常量张量，动态范围是固定的，`activation` 的动态范围是变化的，它的实际动态范围必须经过采样获取（一般把这个过程称为数据校准(`calibration`)）。\n\n将浮点量化转为定点时调整最小值/最大值（**值域调整**），也就是浮点数动态范围的选择，**动态范围的选取直接决定了量化数据的分布情况，处于动态范围之外的数据将被映射成量化数据的边界点，即值域的选择直接决定了量化的误差**。\n\n目前各大深度学习框架和三大平台的推理框架使用最多的有最大最小值（`MinMax`）、滑动平均最大最小值（`MovingAverageMinMax`）和 `KL` 距离（Kullback-Leibler divergence）三种方法，去确定浮点数的动态范围。如果量化过程中的每一个 `FP32` 数值都在这个实际动态范围内，我们一般称这种为不饱和状态；反之如果出现某些 `FP32` 数值不在这个实际动态范围之内我们称之为饱和状态。\n\n### 3.2，最大最小值（MinMax）\n\n`MinMax` 是使用最简单也是较为常用的一种采样方法。基本思想是直接从 `FP32` 张量中选取最大值和最小值来确定实际的动态范围，如下公式所示。\n\n$$x_{min} = \\left\\{\\begin{matrix}min(X) & if\\ x_{min} = None \\\\  min(x_{min}, min(X))  & otherwise\\end{matrix}\\right. \\\\\nx_{max} = \\left\\{\\begin{matrix}max(X) & if\\ x_{max} = None \\\\  max(x_{max}, max(X))  & otherwise\\end{matrix}\\right.$$\n\n对 `weights` 而言，这种采样方法是不饱和的，但是对于 `activation` 而言，如果采样数据中出现离群点，则可能明显扩大实际的动态范围，比如实际计算时 `99%` 的数据都均匀分布在 `[-100, 100]` 之间，但是在采样时有一个离群点的数值为 `10000`，这时候采样获得的动态范围就变成 `[-100, 10000]`。\n\n### 3.3，滑动平均最大最小值(MovingAverageMinMax)\n\n与 `MinMax` 算法直接替换不同，MovingAverageMinMax 会采用一个超参数 `c` (Pytorch 默认值为0.01)逐步更新动态范围。\n\n$$x_{min} = \\left\\{\\begin{matrix}min(X) & if x_{min} = None \\\\ (1-c)x_{min}+c \\; min(X) & otherwise\\end{matrix}\\right.\\\\ \nx_{max} = \\left\\{\\begin{matrix}max(X) & if x_{max} = None \\\\  (1-c)x_{max}+c \\; max(X) & otherwise\\end{matrix}\\right.$$\n\n这种方法获得的动态范围一般要小于实际的动态范围。对于 weights 而言，由于不存在采样的迭代，因此 MovingAverageMinMax 与 MinMax 的效果是一样的。\n\n### 3.4，KL 距离采样方法(Kullback–Leibler divergence)\n\n理解 KL 散度方法之前，我们先看下 `TensorRT` 关于值域范围阈值选择的一张图：\n\n<div align=\"center\">\n<img src=\"../images/quantization/threshold_selection.png\" width=\"60%\" alt=\"阈值选择\">\n</div>\n\n这张图展示的是不同网络结构的不同 `layer` 的激活值分布统计图，横坐标是激活值，纵坐标是统计数量的归一化表示，而不是绝对数值统计；图中有卷积层和池化层，它们之间分布很不相同，因此合理的量化方法应该是适用于不同的激活值分布，并且减小信息损失，因为从 `FP32` 到 `INT8` 其实也是一种信息再编码的过程。\n\n简单的将一个 tensor 中的 `-|max|` 和 `|max|` FP32 value 映射为 -127 和 127 ，中间值按照线性关系进行映射，这种映射关系为不饱和的（No saturation），即对称的。对于这种简单的量化浮点方法，试验结果显示会导致比较大的精度损失。\n\n通过上图可以分析出，线性量化中使用简单的量化浮点方法导致精度损失较大的原因是：\n\n+ 上图的激活值统计针对的是一批图片，不同图片输出的激活值不完全相同，所以图中是多条曲线而不是一条曲线，曲线中前面一部分数据重合在一起了（红色虚线），说明不同图片生成的大部分激活值其分布是相似的；但是在曲线的右边，激活值比较大时（红色实现圈起来的部分），曲线不重复了，一个激活值会对应多个不同的统计量，这时激活值分布是比较乱的。\n+ 曲线后面激活值分布比较乱的部分在整个网络层占是占少数的（比如 $10^-9$, $10^-7$, $10^-3$），因此曲线后面的激活值分布部分可以不考虑到映射关系中，只保留激活值分布的主方向。\n\n一般认为量化之后的数据分布与量化前的数据分布越相似，量化对原始数据信息的损失也就越小，即量化算法精度越高。`KL` 距离(也叫 `KL` 散度)一般被用来度量两个分布之间的相似性。这里的数据分布都是离散形式的，其离散数据的 KL 散度公式如下：\n\n$$D_{KL}(P \\| Q) = \\sum_i P(i)log_{a} \\frac{P(i)}{Q(i)} = \\sum_i P(i)[logP(x) - log Q(x)]$$\n\n式中 P 和 Q 分布表示量化前 FP32 的数据分布和量化后的 INT8 数据分布。注意公式要求 P、Q 两个统计直方图长度一样（也就是 bins 的数量一样）。\n\nTensorRT 使用 KL 散度算法进行量化校准的过程：首先在校准集上运行 FP32 推理，然后对于网络每一层执行以下步骤：\n\n1. 收集激活输出的直方图。\n2. 生成许多具有不同饱和度阈值的量化分布。\n3. 选择最小化 KL_divergence(ref_distr, quant_distr) 的阈值 `T`，并确定 `Scale`。\n\n以上使用校准集的模型量化过程通常只需几分钟时间。\n\n### 3.5，总结\n\n量化方法总结：\n+ 对称的，不饱和的线性量化，会导致精度损失较大；\n+ 通过最小化 `KL` 散度来选择饱和量化中的 阈值 `|T|`;\n\n量化实战经验：\n\n1. 量化是一种已经获得了工业界认可和使用的方法，在训练 (Training) 中使用 `FP32` 精度，在推理 (Inference) 期间使用 `INT8` 精度的这套量化体系已经被包括 `TensorFlow`，`TensorRT`，`PyTorch`，`MxNet` 等众多深度学习框架和启用，地平线机器人、海思、安霸等众多 `AI` 芯片厂商也在深度学习工具链中提供了各自版本的模型量化功能。\n2. 量化是一个大部分硬件平台都会支持的，因此比较常用；知识蒸馏有利于获得小模型，还可以进一步提升量化模型的精度，所以这个技巧也会使用，尤其是在有已经训好比较强的大模型的基础上会非常有用。剪枝用的会相对较少，它和网络结构搜索 NAS 算法功能很像。\n\n## 参考资料\n\n- [NCNN Conv量化详解（一）](https://zhuanlan.zhihu.com/p/71881443)\n- [卷积神经网络优化算法](https://jackwish.net/2019/convolution-neural-networks-optimization.html)\n- [神经网络量化简介](https://jackwish.net/2019/neural-network-quantization-introduction-chn.html)\n- [QNNPACK 实现揭秘](https://jackwish.net/2019/reveal-qnnpack-implementation.html)\n- 《8-bit Inference with TensorRT》\n"
  },
  {
    "path": "5-model_compression/模型压缩-轻量化网络总结.md",
    "content": "- [前言](#前言)\n- [一些关键字理解](#一些关键字理解)\n  - [计算量 FLOPs](#计算量-flops)\n  - [内存访问代价 MAC](#内存访问代价-mac)\n  - [GPU 内存带宽](#gpu-内存带宽)\n  - [Latency and Throughput](#latency-and-throughput)\n  - [Volatile GPU Util](#volatile-gpu-util)\n  - [英伟达 GPU 架构](#英伟达-gpu-架构)\n- [CNN 架构的理解](#cnn-架构的理解)\n- [手动设计高效 CNN 架构建议](#手动设计高效-cnn-架构建议)\n  - [一些结论](#一些结论)\n  - [一些建议](#一些建议)\n- [轻量级网络模型部署总结](#轻量级网络模型部署总结)\n- [轻量级网络论文解析文章汇总](#轻量级网络论文解析文章汇总)\n- [参考资料](#参考资料)\n\n> **文章同步发于 [github 仓库](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/%E8%BD%BB%E9%87%8F%E7%BA%A7%E6%A8%A1%E5%9E%8B%E8%AE%BE%E8%AE%A1%E4%B8%8E%E9%83%A8%E7%BD%B2%E6%80%BB%E7%BB%93.md) 和 [csdn 博客](http://t.csdn.cn/eZ1jp)**，最新板以 `github` 为主。\n> 本人水平有限，文章如有问题，欢迎及时指出。如果看完文章有所收获，一定要先点赞后收藏。毕竟，**赠人玫瑰，手有余香**。\n\n## 前言\n\n**轻量级网络的核心是在尽量保持精度的前提下，从模型体积和速度两方面对网络进行轻量化改造**。关于如何手动设计轻量级网络的研究，目前还没有广泛通用的准则，只有一些指导思想，和针对不同芯片平台（不同芯片架构）的一些设计总结，建议大家从经典论文中吸取指导思想和建议，然后自己实际做各个硬件平台的部署和模型性能测试。\n\n对于卷积神经网络，典型的工作有 `Mobilenet` 系列网络、`ShuffleNet` 系列网络、`RepVGG`、`CSPNet`、`VoVNet` 等论文。轻量级网络论文解析可阅读 [github 专栏-轻量级网络](./3-classic_backbone/efficient_cnn)。\n\n## 一些关键字理解\n### 计算量 FLOPs\n\n- `FLOPs`：`floating point operations` 指的是浮点运算次数，理解为**计算量**，可以用来衡量**算法/模型时间的复杂度**。\n- `FLOPS`：（全部大写），`Floating-point Operations Per Second`，每秒所执行的浮点运算次数，理解为计算速度, 是一个衡量硬件性能/模型速度的指标，即一个芯片的算力。\n- `MACCs`：`multiply-accumulate operations`，乘-加操作次数，`MACCs` 大约是 `FLOPs` 的一半。将 w[0]∗x[0]+... 视为一个乘法累加或 `1` 个 `MACC`。\n\n### 内存访问代价 MAC\n\n`MAC`: `Memory Access Cost` 内存访问代价。指的是输入单个样本（一张图像），模型/卷积层完成**一次前向传播所发生的内存交换总量**，即模型的空间复杂度，单位是 `Byte`。\n\n> `FLOPs` 和 `MAC` 的计算方式，请参考我之前写的文章 [神经网络模型复杂度分析](https://github.com/HarleysZhang/cv_note/blob/79740428b6162630eb80ed3d39052cac52f60c32/9-model_deploy/B-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B%E5%A4%8D%E6%9D%82%E5%BA%A6%E5%88%86%E6%9E%90.md)。\n\n### GPU 内存带宽\n\n- `GPU` 的内存带宽决定了它将数据从内存 (`vRAM`) 移动到计算核心的速度，是比 `GPU` 内存速度更具代表性的指标。\n- `GPU` 的内存带宽的值取决于**内存和计算核心之间的数据传输速度，以及这两个部分之间总线中单独并行链路的数量**。\n\n`NVIDIA RTX A4000` 建立在 `NVIDIA`  `Ampere` 架构之上，其芯片规格如下所示:\n\n![](../images/efficient_model/A4000_specifications.png)\n\n`A4000` 芯片配备 `16 GB` 的 `GDDR6` 显存、`256` 位显存接口（`GPU` 和 `VRAM` 之间总线上的独立链路数量），因为这些与显存相关的特性，所以 `A4000` 内存带宽可以达到 `448 GB/s`。\n\n\n### Latency and Throughput\n> 参考英伟达-Ashu RegeDirector of Developer Technology 的 `ppt` 文档 [An Introduction to Modern GPU Architecture](https://download.nvidia.com/developer/cuda/seminar/TDCI_Arch.pdf)。\n\n深度学习领域**延迟** `Latency` 和**吞吐量** `Throughput`的一般解释：\n\n+ 延迟 (`Latency`): 人和机器做决策或采取行动时都需要反应时间。延迟是指**提出请求与收到反应之间经过的时间**。大部分人性化软件系统（不只是 AI 系统），延迟都是以**毫秒**来计量的。\n+ 吞吐量 (`Throughput`): 在给定创建或部署的深度学习网络规模的情况下，可以传递多少推断结果。简单理解就是在**一个时间单元（如：一秒）内网络能处理的最大输入样例数**。\n\n**`CPU` 是低延迟低吞吐量处理器；`GPU` 是高延迟高吞吐量处理器**。\n\n### Volatile GPU Util\n\n一般，很多人通过 `nvidia-smi` 命令查看 `Volatile GPU Util` 数据来得出 `GPU` 利用率，但是！关于这个利用率(`GPU Util`)，容易产生**两个误区**：\n\n- 误区一: `GPU` 的利用率 = `GPU` 内计算单元干活的比例。利用率越高，算力就必然发挥得越充分。\n- 误区二: 同条件下，利用率越高，耗时一定越短。\n\n但实际上，`GPU Util` 的本质只是反应了，在采样时间段内，一个或多个内核（`kernel`）在 `GPU` 上执行的时间百分比，采样时间段取值 `1/6s~1s`。\n> 原文为 Percent of time over the past sample period during which one or more kernels was executing on the GPU. The sample period may be between 1 second and 1/6 second depending on the product. 来源文档 [nvidia-smi.txt](https://developer.download.nvidia.com/compute/DCGM/docs/nvidia-smi-367.38.pdf)\n\n通俗来讲，就是，在一段时间范围内， `GPU` 内核运行的时间占总时间的比例。比如 `GPU Util` 是 `69%`，时间段是 `1s`，那么在过去的 `1s` 中内，`GPU` 内核运行的时间是 `0.69s`。如果 `GPU Util` 是 `0%`，则说明 `GPU` 没有被使用，处于空闲中。\n\n也就是说**它并没有告诉我们使用了多少个 `SM` 做计算，或者程序有多“忙”，或者内存使用方式是什么样的，简而言之即不能体现出算力的发挥情况**。\n> `GPU Util` 的本质参考知乎文章-[教你如何继续压榨GPU的算力](https://zhuanlan.zhihu.com/p/346389176) 和 [stackoverflow 问答](https://stackoverflow.com/questions/40937894/nvidia-smi-volatile-gpu-utilization-explanation)。\n\n### 英伟达 GPU 架构\n\n`GPU` 设计了更多的晶体管（`transistors`）用于数据处理（`data process`）而不是数据缓冲（`data caching`）和流控（`flow control`），因此 `GPU` 很适合做**高度并行计算**（`highly parallel computations`）。同时，`GPU` 提供比 `CPU` 更高的**指令吞吐量**和**内存带宽**（`instruction throughput and memory bandwidth`）。\n\n`CPU` 和 `GPU` 的直观对比图如下所示\n\n![distribution of chip resources for a CPU versus a GPU](../images/efficient_model/gpu-devotes-more-transistors-to-data-processing.png)\n> 图片来源 [CUDA C++ Programming Guide](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html)\n\n最后简单总结下英伟达 `GPU` 架构的一些特点:\n- `SIMT` (`Single Instruction Multiple Threads`) 模式，即多个 `Core` 同一时刻只能执行同样的指令。虽然看起来与现代 `CPU` 的 `SIMD`（单指令多数据）有些相似，但实际上有着根本差别。\n- 更适合**计算密集**与**数据并行**的程序，原因是缺少 `Cache` 和 `Control`。\n\n`2008-2020` 英伟达 `GPU` 架构进化史如下图所示:\n\n![`2008-2020` 英伟达 `GPU` 架构进化史](../images/efficient_model/gpu_architecture.png)\n\n另外，英伟达 `GPU` 架构从 `2010` 年开始到 `2020` 年这十年间的架构演进历史概述，可以参考知乎的文章-[英伟达GPU架构演进近十年，从费米到安培](https://zhuanlan.zhihu.com/p/413145211)。\n\n`GPU` 架构的深入理解可以参考博客园的文章-[深入GPU硬件架构及运行机制](https://www.cnblogs.com/timlly/p/11471507.html#41-gpu%E6%B8%B2%E6%9F%93%E6%80%BB%E8%A7%88)。\n\n## CNN 架构的理解\n\n**网络宽度**，即通道(`channel`)的数量，也是滤波器（3 维）的数量；**网络深度**，即 `layer` 的层数，如 `resnet18` 有 `18` 层网络。注意我们这里说的网络宽度和宽度学习一类的模型没有关系，而是特指深度卷积神经网络的（通道）宽度。\n\n- **网络深度的意义**：`CNN` 的网络层能够对输入图像数据进行逐层抽象，比如第一层学习到了图像边缘特征，第二层学习到了简单形状特征，第三层学习到了目标形状的特征。即**深度决定了网络的表达（抽象）能力，网络越深学习能力越强**。\n- **网络宽度的意义**：网络越宽，对目标特征的提取能力越强，即这一层网络能学习到更加丰富的特征，比如不同方向、不同频率的纹理特征等。即**宽度决定了网络在某一层学到的信息量**。\n\n总而言之，在一定的程度上，网络越深越宽，性能越好，但关键在于如何取 `trade-off`。\n## 手动设计高效 CNN 架构建议\n\n### 一些结论\n\n1. **分析模型的推理性能得结合具体的推理平台**（常见如：英伟达 `GPU`、移动端 `ARM` `CPU`、端侧 `NPU` 芯片等）；目前已知影响 `CNN` 模型推理性能的因素包括: 算子计算量 `FLOPs`（参数量 `Params`）、卷积 `block` 的内存访问代价（访存带宽）、网络并行度等。但相同硬件平台、相同网络架构条件下， `FLOPs` 加速比与推理时间加速比成正比。\n2. 建议对于轻量级网络设计应该考虑直接 `metric`（例如速度 `speed`），而不是间接 `metric`（例如 `FLOPs`）。\n3. **`FLOPs` 低不等于 `latency` 低，尤其是在有加速功能的硬体 (`GPU`、`DSP` 与 `TPU`)上不成立，得结合具硬件架构具体分析**。\n4. 不同网络架构的 `CNN` 模型，即使是 `FLOPs` 相同，但其 `MAC` 也可能差异巨大。\n5. **`Depthwise` 卷积操作对于流水线型 `CPU`、`ARM` 等移动设备更友好，对于并行计算能力强的 `GPU` 和具有加速功能的硬件（专用硬件设计-NPU 芯片）上比较没有效率**。`Depthwise` 卷积算子实际上是使用了大量的低 `FLOPs`、高数据读写量的操作。因为这些具有高数据读写量的操作，再加上**多数时候  `GPU` 芯片算力的瓶颈在于访存带宽**，使得模型把大量的时间浪费在了从显存中读写数据上，从而导致 `GPU` 的算力没有得到“充分利用”。结论来源知乎文章-[FLOPs与模型推理速度](https://zhuanlan.zhihu.com/p/122943688)和论文 [G-GhostNet](https://arxiv.org/pdf/2201.03297.pdf)。\n\n### 一些建议\n\n1. 在大多数的硬件上，`channel` 数为 `16` 的倍数比较有利高效计算。如海思 `351x` 系列芯片，当输入通道为 `4` 倍数和输出通道数为 `16` 倍数时，时间加速比会近似等于 `FLOPs` 加速比，有利于提供 `NNIE` 硬件计算利用率。(来源海思 `351X` 芯片文档和 `MobileDets` 论文)\n2. 低 `channel` 数的情况下 (如网路的前几层)，在有加速功能的硬件使用普通 `convolution` 通常会比 `separable convolution` 有效率。（来源 [MobileDets 论文](https://medium.com/ai-blog-tw/mobiledets-flops%E4%B8%8D%E7%AD%89%E6%96%BClatency-%E8%80%83%E9%87%8F%E4%B8%8D%E5%90%8C%E7%A1%AC%E9%AB%94%E7%9A%84%E9%AB%98%E6%95%88%E6%9E%B6%E6%A7%8B-5bfc27d4c2c8)）\n3. [shufflenetv2 论文](https://arxiv.org/pdf/1807.11164.pdf) 提出的**四个高效网络设计的实用指导思想**: G1同样大小的通道数可以最小化 `MAC`、G2-分组数太多的卷积会增加 `MAC`、G3-网络碎片化会降低并行度、G4-逐元素的操作不可忽视。\n4. `GPU` 芯片上 $3\\times 3$ 卷积非常快，其计算密度（理论运算量除以所用时间）可达 $1\\times 1$ 和 $5\\times 5$ 卷积的四倍。（来源 [RepVGG 论文](https://zhuanlan.zhihu.com/p/344324470)）\n5. **从解决梯度信息冗余问题入手**，提高模型推理效率。比如 [CSPNet](https://arxiv.org/pdf/1911.11929.pdf) 网络。\n6. 从解决 `DenseNet` 的密集连接带来的高内存访问成本和能耗问题入手，如 [VoVNet](https://arxiv.org/pdf/1904.09730.pdf) 网络，其由 `OSA`（`One-Shot Aggregation`，一次聚合）模块组成。\n\n## 轻量级网络模型部署总结\n\n在阅读和理解经典的轻量级网络 `mobilenet` 系列、`MobileDets`、`shufflenet` 系列、`cspnet`、`vovnet`、`repvgg` 等论文的基础上，做了以下总结：\n\n1. 低算力设备-手机移动端 `cpu` 硬件，考虑 `mobilenetv1`(深度可分离卷机架构-低 `FLOPs`)、低 `FLOPs` 和 低`MAC`的`shuffletnetv2`（`channel_shuffle` 算子在推理框架上可能不支持）\n2. 专用 `asic` 硬件设备-`npu` 芯片（地平线 `x3/x4` 等、海思 `3519`、安霸`cv22` 等），分类、目标检测问题考虑 `cspnet` 网络(减少重复梯度信息)、`repvgg2`（即 `RepOptimizer`: `vgg` 型直连架构、部署简单）\n3. 英伟达 `gpu` 硬件-`t4` 芯片，考虑 `repvgg` 网络（类 `vgg` 卷积架构-高并行度有利于发挥 `gpu` 算力、单路架构省显存/内存，问题: `INT8 PTQ` 掉点严重）\n\n`MobileNet block` (深度可分离卷积 `block`, `depthwise separable convolution block`)在有加速功能的硬件（专用硬件设计-`NPU` 芯片）上比较没有效率。\n> 这个结论在 [CSPNet](https://arxiv.org/pdf/1911.11929.pdf) 和 [MobileDets](https://arxiv.org/pdf/2004.14525.pdf) 论文中都有提到。\n\n除非芯片厂商做了定制优化来提高深度可分离卷积 `block` 的计算效率，比如地平线机器人 `x3` 芯片对深度可分离卷积 `block` 做了定制优化。\n\n下表是 `MobileNetv2` 和 `ResNet50` 在一些常见 `NPU` 芯片平台上做的性能测试结果。\n\n![深度可分离卷积和常规卷积模型在不同NPU芯片平台上的性能测试结果](../images/efficient_model/model_perf.png)\n\n\n以上，均是看了轻量级网络论文总结出来的一些**不同硬件平台部署轻量级模型的经验**，实际结果还需要自己手动运行测试。\n\n## 轻量级网络论文解析文章汇总\n\n- [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)\n- [ShuffleNetv2论文详解](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/ShuffleNetv2%E8%AE%BA%E6%96%87%E8%AF%A6%E8%A7%A3.md)\n- [RepVGG论文详解](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/RepVGG%E8%AE%BA%E6%96%87%E8%AF%A6%E8%A7%A3.md)\n- [CSPNet论文详解](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/CSPNet%E8%AE%BA%E6%96%87%E8%AF%A6%E8%A7%A3.md)\n- [VoVNet论文解读](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/VoVNet%E8%AE%BA%E6%96%87%E8%A7%A3%E8%AF%BB.md)\n\n## 参考资料\n\n- [An Introduction to Modern GPU Architecture](https://download.nvidia.com/developer/cuda/seminar/TDCI_Arch.pdf)\n- [轻量级网络论文解析合集](https://github.com/HarleysZhang/cv_note/tree/79740428b6162630eb80ed3d39052cac52f60c32/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)\n"
  },
  {
    "path": "5-model_compression/深度学习模型压缩方法概述.md",
    "content": "## 一，模型压缩技术概述\n\n我们知道，一定程度上，网络越深，参数越多，模型也会越复杂，但其最终效果也越好，而模型压缩算法是旨在将一个庞大而复杂的大模型转化为一个精简的小模型。\n\n之所以必须做模型压缩，是因为嵌入式设备的**算力和内存有限**，经过压缩后的模型方才能部署到嵌入式设备上。\n\n模型压缩问题的定义可以从 `3` 角度出发:\n\n1. 模型压缩的收益:\n   - **计算**: 减少浮点运算量（`FLOPs`），降低延迟（`Latency`）\n   - **存储**: 减少内存占用，提高 `GPU/NPU` 计算利用率\n\n2. 公式定义模型压缩问题: $\\underset{Policy_i}{min} {Model\\_Size(Policy_i)}$\n3. 模型压缩问题的约束: $accuracy(Policy_i) >= accuracy\\_sla$\n\n### 1.1，模型压缩方法分类\n\n按照压缩过程对网络结构的破坏程度，《解析卷积神经网络》一书中将模型压缩技术分为“前端压缩”和“后端压缩”两部分:\n\n- 前端压缩，是指在不改变原网络结构的压缩技术，主要包括`知识蒸馏`、轻量级网络（紧凑的模型结构设计）以及`滤波器（filter）层面的剪枝（结构化剪枝）`等；\n- 后端压缩，是指包括`低秩近似`、未加限制的剪枝（非结构化剪枝/稀疏）、`参数量化`以及二值网络等，目标在于尽可能减少模型大小，会对原始网络结构造成极大程度的改造。\n\n总结：前端压缩几乎不改变原有网络结构（仅仅只是在原模型基础上减少了网络的层数或者滤波器个数），后端压缩对网络结构有不可逆的大幅度改变，造成原有深度学习库、甚至硬件设备不兼容改变之后的网络。其维护成本很高。\n\n### 1.2，模型压缩方法举例\n\n工业界主流的模型压缩方法有：知识蒸馏（Knowledge Distillation，KD）轻量化模型架构（也叫紧凑的模型设计）、剪枝（Pruning）、量化（Quantization）。各个模型压缩方法总结如下：\n\n| 模型压缩方法   | 描述                                                         | 涉及的网络层               | 示例                                                         |\n| -------------- | ------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------ |\n| 知识蒸馏       | 属于迁移学习的一种，主要思想是将学习能力强的复杂教师模型中的“知识”迁移到简单的学生模型中。 | 卷积和全连接层             | 经典KD论文，属于蒸 \"logits\"方法，将Teacher Network输出的soft label作为标签来训练Student Network。必须重新训练模型。 |\n| 轻量化模型架构 | 轻量级网络的核心是在尽量保持精度的前提下，从体积和速度两方面对网络进行轻量化改造。 | 卷积层/卷积模块            | Mobilenet 提出深度可分离卷积；<br />[shufflenetv2 论文](https://link.zhihu.com/?target=https%3A//arxiv.org/pdf/1807.11164.pdf) 提出的**四个高效网络设计的实用指导思想**；<br />RepVGG 提出重参数化思想。<br />都需要重新设计 `backbone` 和和重新训练模型。 |\n| 剪枝           | 将权重低于阈值的连接都从网络中删除。                         | 卷积层和全连接层           | 韩松2016年Deep Compression属于开山之作，剪枝步骤：正常训练，删除网络中权重低于阈值的连接层，重新训练。需要重新训练模型。 |\n| 量化           | 指将神经网络的浮点算法转换为定点算法                         | 卷积、全连接、激活、BN层等 | TensoRT框架中的基于 KL 散度方法的INT8量化策略是主流技术。`PTQ` 训练后量化方法不需要重新训练模型。 |\n\n## 二，知识蒸馏\n\n知识蒸馏（knowledge distillation），其实也属于迁移学习（transfer learning）的一种，通俗理解就是训练一个大模型（teacher 模型）和一个小模型（student 模型），将庞大而复杂的大模型学习到的知识，通过一定技术手段迁移到精简的小模型上，从而使小模型能够获得与大模型相近的性能。也可说让小模型去拟合大模型，从而让**小模型学到与大模型相似的函数映射**。使其保持其快速的计算速度前提下，同时拥有复杂模型的性能，达到模型压缩的目的。\n\n知识蒸馏的关键在于监督特征的设计，这个领域的开篇之作-[Distilling the Knowledge in a Neural Network](https://link.zhihu.com/?target=https%3A//arxiv.org/abs/1503.02531) 使用 `Soft Target` 所提供的类间相似性作为依据去指导小模型训练（`软标签蒸馏 KD`）。后续工作也有使用大模型的中间层特征图或 attention map（`features KD` 方法）作为监督特征，对小模型进行指导训练。这个领域的开篇之作-Distilling the Knowledge in a Neural Network，是属于软标签 KD 方法，后面还出现了 features KD 的论文。\n\n以经典的知识蒸馏实验为例，我们先训练好一个 `teacher` 网络，然后将 `teacher` 的网络的输出结果 $q$ 作为 `student` 网络的目标，训练 `student` 网络，使得 `student` 网络的结果 $p$ 接近 $q$ ，因此，`student` 网络的损失函数为 $L = CE(y,p)+\\alpha CE(q,p)$。这里 `CE` 是交叉熵（Cross Entropy），$y$ 是真实标签的 `onehot` 编码，$q$ 是 `teacher` 网络的输出结果，$p$ 是 `student` 网络的输出结果。\n\n但是，直接使用 `teacher` 网络的 softmax 的输出结果 $q$，可能不大合适。因为，一个网络训练好之后，对于正确的答案会有一个很高的置信度而错误答案的置信度会很小。例如，在 MNIST 数据中，对于某个 2 的输入，对于 2 的预测概率会很高，而对于 2 类似的数字，例如 3 和 7 的预测概率为 $10^-6$ 和 $10^-9$。这样的话，`teacher` 网络学到**数据的相似信息**（例如数字 2 和 3，7 很类似）很难传达给 `student` 网络，因为它们的概率值接近`0`。因此，论文提出了 `softmax-T`(软标签计算公式)，公式如下所示：\n$$q_{i} = \\frac{z_{i}/T}{\\sum_{j}z_{j}/T}$$\n\n这里 $q_i$ 是 $student$ 网络学习的对象（soft targets），$z_i$ 是 `teacher` 模型 `softmax` 前一层的输出 `logit`。如果将 $T$ 取 1，上述公式**等同于 softmax**，根据 logit 输出各个类别的概率。如果 $T$ 接近于 0，则最大的值会越近 1，其它值会接近 0，近似于 `onehot` 编码。\n\n所以，可以知道 `student` 模型最终的损失函数由两部分组成：\n\n+ 第一项是由小模型的预测结果与大模型的“软标签”所构成的交叉熵（cross entroy）;\n+ 第二项为预测结果与普通类别标签的交叉熵。\n\n这两个损失函数的重要程度可通过一定的权重进行调节，在实际应用中，`T` 的取值会影响最终的结果，一般而言，较大的 T 能够获得较高的准确度，T（蒸馏温度参数） 属于知识蒸馏模型训练超参数的一种。**T 是一个可调节的超参数、T 值越大、概率分布越软（论文中的描述），曲线便越平滑**，相当于在迁移学习的过程中添加了扰动，从而使得学生网络在借鉴学习的时候更有效、泛化能力更强，这其实就是一种抑制过拟合的策略。\n\n知识蒸馏算法整体的框架图如图下所示。\n\n![knowledge_distillation](../images/model_compression/knowledge_distillation.png)\n> 图片来源 https://intellabs.github.io/distiller/knowledge_distillation.html。\n\n## 三，轻量级模型架构\n\n**轻量级网络的核心是在尽量保持精度的前提下，从体积和速度两方面对网络进行轻量化改造**。关于如何手动设计轻量级网络的研究，目前还没有广泛通用的准则，只有一些指导思想，和针对不同芯片平台（不同芯片架构）的一些设计总结，建议大家从经典论文中吸取指导思想和建议，然后自己实际做各个硬件平台的部署和模型性能测试。\n\n对于卷积神经网络，典型的工作有 `Mobilenet` 系列网络、`ShuffleNet` 系列网络、`RepVGG`、`CSPNet`、`VoVNet` 等论文。轻量级网络论文解析可阅读 [github 专栏-轻量级网络](https://github.com/HarleysZhang/cv_note/tree/master/7-model_compression)。\n\n### 3.1，如何设计高效CNN架构\n\n#### 一些结论\n\n1. **分析模型的推理性能得结合具体的推理平台**（常见如：英伟达 `GPU`、移动端 `ARM` `CPU`、端侧 `NPU` 芯片等）；目前已知影响 `CNN` 模型推理性能的因素包括: 算子计算量 `FLOPs`（参数量 `Params`）、卷积 `block` 的内存访问代价（访存带宽）、网络并行度等。但相同硬件平台、相同网络架构条件下， `FLOPs` 加速比与推理时间加速比成正比。\n2. 建议对于轻量级网络设计应该考虑直接 `metric`（例如速度 `speed`），而不是间接 `metric`（例如 `FLOPs`）。\n3. **`FLOPs` 低不等于 `latency` 低，尤其是在有加速功能的硬体 (`GPU`、`DSP` 与 `TPU`)上不成立，得结合具硬件架构具体分析**。\n4. 不同网络架构的 `CNN` 模型，即使是 `FLOPs` 相同，但其 `MAC` 也可能差异巨大。\n5. **`Depthwise` 卷积操作对于流水线型 `CPU`、`ARM` 等移动设备更友好，对于并行计算能力强的 `GPU` 和具有加速功能的硬件（专用硬件设计-NPU 芯片）上比较没有效率**。`Depthwise` 卷积算子实际上是使用了大量的低 `FLOPs`、高数据读写量的操作。因为这些具有高数据读写量的操作，再加上**多数时候  `GPU` 芯片算力的瓶颈在于访存带宽**，使得模型把大量的时间浪费在了从显存中读写数据上，从而导致 `GPU` 的算力没有得到“充分利用”。结论来源知乎文章-[FLOPs与模型推理速度](https://zhuanlan.zhihu.com/p/122943688)和论文 [G-GhostNet](https://arxiv.org/pdf/2201.03297.pdf)。\n\n#### 一些建议\n\n1. 在大多数的硬件上，`channel` 数为 `16` 的倍数比较有利高效计算。如海思 `351x` 系列芯片，当输入通道为 `4` 倍数和输出通道数为 `16` 倍数时，时间加速比会近似等于 `FLOPs` 加速比，有利于提供 `NNIE` 硬件计算利用率。(来源海思 `351X` 芯片文档和 `MobileDets` 论文)\n2. 低 `channel` 数的情况下 (如网路的前几层)，在有加速功能的硬件使用普通 `convolution` 通常会比 `separable convolution` 有效率。（来源 [MobileDets 论文](https://medium.com/ai-blog-tw/mobiledets-flops%E4%B8%8D%E7%AD%89%E6%96%BClatency-%E8%80%83%E9%87%8F%E4%B8%8D%E5%90%8C%E7%A1%AC%E9%AB%94%E7%9A%84%E9%AB%98%E6%95%88%E6%9E%B6%E6%A7%8B-5bfc27d4c2c8)）\n3. [shufflenetv2 论文](https://arxiv.org/pdf/1807.11164.pdf) 提出的**四个高效网络设计的实用指导思想**: G1同样大小的通道数可以最小化 `MAC`、G2-分组数太多的卷积会增加 `MAC`、G3-网络碎片化会降低并行度、G4-逐元素的操作不可忽视。\n4. `GPU` 芯片上 $3\\times 3$ 卷积非常快，其计算密度（理论运算量除以所用时间）可达 $1\\times 1$ 和 $5\\times 5$ 卷积的四倍。（来源 [RepVGG 论文](https://zhuanlan.zhihu.com/p/344324470)）\n5. **从解决梯度信息冗余问题入手**，提高模型推理效率。比如 [CSPNet](https://arxiv.org/pdf/1911.11929.pdf) 网络。\n6. 从解决 `DenseNet` 的密集连接带来的高内存访问成本和能耗问题入手，如 [VoVNet](https://arxiv.org/pdf/1904.09730.pdf) 网络，其由 `OSA`（`One-Shot Aggregation`，一次聚合）模块组成。\n\n## 3.2，轻量级模型部署总结\n\n在阅读和理解经典的轻量级网络 `mobilenet` 系列、`MobileDets`、`shufflenet` 系列、`cspnet`、`vovnet`、`repvgg` 等论文的基础上，做了以下总结：\n\n1. 低算力设备-手机移动端 `cpu` 硬件，考虑 `mobilenetv1`(深度可分离卷机架构-低 `FLOPs`)、低 `FLOPs` 和 低`MAC`的`shuffletnetv2`（`channel_shuffle` 算子在推理框架上可能不支持）\n2. 专用 `asic` 硬件设备-`npu` 芯片（地平线 `x3/x4` 等、海思 `3519`、安霸`cv22` 等），分类、目标检测问题考虑 `cspnet` 网络(减少重复梯度信息)、`repvgg2`（即 `RepOptimizer`: `vgg` 型直连架构、部署简单）\n3. 英伟达 `gpu` 硬件-`t4` 芯片，考虑 `repvgg` 网络（类 `vgg` 卷积架构-高并行度有利于发挥 `gpu` 算力、单路架构省显存/内存，问题: `INT8 PTQ` 掉点严重）\n\n`MobileNet block` (深度可分离卷积 `block`, `depthwise separable convolution block`)在有加速功能的硬件（专用硬件设计-`NPU` 芯片）上比较没有效率。\n> 这个结论在 [CSPNet](https://arxiv.org/pdf/1911.11929.pdf) 和 [MobileDets](https://arxiv.org/pdf/2004.14525.pdf) 论文中都有提到。\n\n除非芯片厂商做了定制优化来提高深度可分离卷积 `block` 的计算效率，比如地平线机器人 `x3` 芯片对深度可分离卷积 `block` 做了定制优化。\n\n下表是 `MobileNetv2` 和 `ResNet50` 在一些常见 `NPU` 芯片平台上做的性能测试结果。\n\n![深度可分离卷积和常规卷积模型在不同NPU芯片平台上的性能测试结果](../images/model_compression/model_perf_result.png)\n\n\n以上，均是看了轻量级网络论文总结出来的一些**不同硬件平台部署轻量级模型的经验**，实际结果还需要自己手动运行测试。\n\n## 四，模型剪枝\n> 模型剪枝（model pruning）也叫模型稀疏化（model sparsity）。\n\n深度学习模型中一般存在着大量冗余的参数，将权重矩阵中相对“不重要”的权值剔除（即置为 `0`），可达到降低计算资源消耗和提高实时性的效果，而对应的技术则被称为模型剪枝。\n\n![典型模型剪枝图例](../images/model_compression/model_pruning_pipeline.png)\n\n> 来源论文 [Han et al. Learning both Weights and Connections for Efficient Neural Networks, NIPS 2015](https://arxiv.org/pdf/1506.02626.pdf)。\n\n上图是典型的三段式剪枝算法 `pipeline`，主要是 3 个步骤：\n\n1. 正常训练模型；\n2. 模型剪枝；\n3. 重新训练模型\n\n以上三个步骤反复迭代进行，直到模型精度达到目标，则停止训练。\n\n模型剪枝算法根据**粒度**的不同，可以粗分为**细粒度剪枝**和**粗粒度剪枝**，如下所示：\n\n1. **细粒度剪枝(fine-grained)**：对连接或者神经元进行剪枝，它是粒度最小的剪枝。\n2. **向量剪枝(vector-level)**：它相对于细粒度剪枝粒度更大，属于对卷积核内部(intra-kernel)的剪枝。\n3. **核剪枝(kernel-level)**：去除某个卷积核，它将丢弃对输入通道中对应计算通道的响应。\n4. **滤波器剪枝(Filter-level)**：也叫通道剪枝（`Channel Pruning`），对整个卷积核组进行剪枝，会造成推理过程中输出特征通道数的改变，滤波器剪枝的工作是目前研究最多的。\n\n按照剪枝是否规则，剪枝算法也可分为：\n- **非结构化剪枝**，其实就是前面的 1，对硬件支持不友好，需要设定特定硬件加速器，典型代表作是韩松 `2016` 年的论文。\n- **结构化剪枝**，如 2、3、4，对硬件支持友好，典型代表作如 [Learning Efficient Convolutional Networks through Network Slimming](https://arxiv.org/pdf/1708.06519.pdf) 等。\n\n### 4.1，结构化稀疏与非结构化剪枝比较\n\n与非结构化剪枝相比，结构化剪枝通常通常会牺牲模型的准确率和压缩比。结构化稀疏对非零权值的位置进行了限制，在剪枝过程中会将一些数值较大的权值剪枝，从而影响模型准确率。 “非规则”的剪枝则契合了神经网络模型中不同大小权值的**随机分布**，这对深度学习模型的准确度至关重要。展开来讲就是：\n\n1. 非结构化稀疏具有更高的模型压缩率和准确性，在通用硬件上的加速效果不好。因为其计算特征上的“不规则”，导致需要特定硬件支持才能实现加速效果。\n2. 结构化稀疏虽然牺牲了模型压缩率或准确率，但在通用硬件上的加速效果好，所以其被广泛应用。因为结构化稀疏使得权值矩阵更规则更加结构化，更利于硬件加速。\n\n![Unstructured_structured_sparsity](../images/model_compression/Unstructured_structured_sparsity.png)\n\n综上所述，深度神经网络的权值稀疏应该在**模型有效性和计算高效性之间做权衡**。\n\n目前，有一种趋势是在软硬件上都支持**稀疏张量**，因此未来非结构化剪枝可能会变得更流行。\n\n## 五，模型量化\n\n相比于剪枝操作，参数量化则是一种常用的后端压缩技术。所谓**量化**，其实可以等同于**低精度**（Low precision）运算概念，常规模型精度一般使用 FP32（32 位浮点数，单精度）存储模型权重参数，低精度则表示使用 `INT8`、`FP16` 等权重数值格式。\n\n模型量化（`Model Quantization`，也叫网络量化）过程分为两部分：将模型的**单精度参数**（一般 `FP32`-`32`位**浮点**参数）转化为**低精度参数**（一般 `INT8`-`8` 位**定点**参数），以及模型推理过程中的浮点运算转化为定点运算，这个需要推理框架支持。\n\n模型量化技术可以降低模型的存储空间、内存占用和计算资源需求，从而提高模型的推理速度，也是为了更好的适配移动端/端侧 `NPU` 加速器。简单总结就是，模型变小了，速度变快了，支持的场景更多了。\n\n最后，现在工业界主流的思路就是模型训练使用高精度-FP32 参数模型，模型推理使用低精度-INT8 参数模型: 将模型从 FP32 转换为 INT8（即量化算术过程），以及使用 INT8 进行推理。\n\n### 5.1，模型量化的方案\n\n\n在实践中将浮点模型转为量化模型的方法有以下三种方法：\n\n1. `data free`：不使用校准集，传统的方法直接将浮点参数转化成量化数，使用上非常简单，但是一般会带来很大的精度损失，但是高通最新的论文 `DFQ` 不使用校准集也得到了很高的精度。\n2. `calibration`：基于校准集方案，通过输入少量真实数据进行统计分析。很多芯片厂商都提供这样的功能，如 `tensorRT`、高通、海思、地平线、寒武纪\n3. `finetune`：基于训练 `finetune` 的方案，将量化误差在训练时仿真建模，调整权重使其更适合量化。好处是能带来更大的精度提升，缺点是要修改模型训练代码，开发周期较长。\n\n按照量化阶段的不同，量化方法分为以下两种：\n\n- Post-training quantization `PTQ`（训练后量化、离线量化）；\n- Quantization-aware training `QAT`（训练时量化，伪量化，在线量化）。\n\n### 5.2，量化的分类\n\n目前已知的加快推理速度概率较大的量化方法主要有：\n\n1. **二值化**，其可以用简单的位运算来同时计算大量的数。对比从 nvdia gpu 到 x86 平台，1bit 计算分别有 5 到128倍的理论性能提升。且其只会引入一个额外的量化操作，该操作可以享受到 SIMD（单指令多数据流）的加速收益。\n2. **线性量化**(最常见)，又可细分为非对称，对称和 `ristretto` 几种。在 `nvdia gpu`，`x86`、`arm` 和 部分 `AI` 芯片平台上，均支持 `8bit` 的计算，效率提升从 `1` 倍到 `16` 倍不等，其中 `tensor core` 甚至支持 `4bit`计算，这也是非常有潜力的方向。线性量化引入的额外量化/反量化计算都是标准的向量操作，因此也可以使用 `SIMD` 进行加速，带来的额外计算耗时不大。\n3. **对数量化**，一种比较特殊的量化方法。两个同底的幂指数进行相乘，那么等价于其指数相加，降低了计算强度。同时加法也被转变为索引计算。目前 `nvdia gpu`，`x86`、`arm` 三大平台上没有实现对数量化的加速库，但是目前已知海思 `351X` 系列芯片上使用了对数量化。\n\n## 六，压缩方法总结\n\n1. 按照剪枝是否规则，剪枝算法可分为：**非结构化剪枝**和**结构化剪枝**。前者可以保持高模型压缩率和准确率，但难加速；后者更利于硬件加速，但牺牲了模型压缩率或准确率。 \n2. 如果需要一次性端对端训练得到压缩与加速后模型，可以考虑基于轻量化网络设计的模型进行模型压缩与加速。\n3. 影响神经网络推理速度主要有 `4` 个因素：计算量 `FLOPs`、内存访问代价 `MAC`、计算并行度、硬件平台架构与特性（算力、GPU 内存带宽）。\n\n## 参考资料\n\n1. [深度学习模型压缩与加速](https://www.cnblogs.com/LXP-Never/p/14833772.html)\n2. [Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding](https://arxiv.org/pdf/1510.00149.pdf)\n3. 《解析卷积神经网络》\n4. 《4-model_compression》(https://github.com/HarleysZhang/deep_learning_system/tree/main/4-model_compression)"
  },
  {
    "path": "6-model_deploy/AI芯片速览.md",
    "content": "## 一，TPU\n\n张量处理单元 (TPU) 是 Google 设计的**机器学习加速器**，支持  [TensorFlow](https://www.tensorflow.org/?hl=zh-cn)、[Pytorch](https://cloud.google.com/tpu/docs/tutorials/pytorch-pod?hl=zh-cn) 和 [JAX](https://jax.readthedocs.io/en/latest/ 机器学习框架。\n\nTPU 芯片上的每个 `TensorCore` 都由一个或多个矩阵乘法单元 (`MXU`)、向量和标量单元组成。\n\n`MXU` 由[脉动阵列](https://en.wikipedia.org/wiki/Systolic_array)中的 $128 \\times 128$ 乘法/累加器组成。MXU 可在 TensorCore 中提供大部分计算能力。每个 MXU 能够在每个周期中执行 16K 乘法累加运算。所有乘法都接受 [bfloat16](https://cloud.google.com/tpu/docs/bfloat16?hl=zh-cn) 输入，但所有累积都以 FP32 数字格式执行。\n\n矢量单位用于激活和 `softmax` 等常规计算。标量单元用于控制流、计算内存地址和其他维护操作。\n\n### 1.1，TPU v4\n\nTPU v2 于 2018 年上市，TPU v3 于 2019 年上市，TPU v4 于 2020 年推出，其中 TPU v4 是 Google **最新一代**的自定义机器学习加速器。\n\n每个 v4 TPU 芯片包含两个 TensorCore。每个 TensorCore 都有四个 MXU、一个矢量单位和一个标量单位。下表显示了 v4 TPU Pod 的主要规范及其值。\n\n| **主要规范**             | **v4 Pod 值**                      |\n| :----------------------- | :--------------------------------- |\n| 每个芯片的峰值计算       | 275 万亿次浮点运算（bf16 或 int8） |\n| HBM2 容量和带宽          | 32 GiB、1200 GBps                  |\n| 测量的最小/平均/最大功率 | 90/170/192 瓦                      |\n| TPU Pod 大小             | 4096 条状标签                      |\n| 互连拓扑                 | 3D 环面                            |\n| 每个 Pod 的峰值计算      | 1.1 万亿次浮点运算（bf16 或 int8） |\n| 每个 Pod 的全宽带宽      | 1.1 PB/秒                          |\n| 每个 Pod 的对分带宽      | 24 TB/秒                           |\n\n> 275 万亿次浮点运算就是 275T 算力，这算力有点夸张啊。\n\n![tpu-v4-layout](../images/ai_chips/tpu-v4-layout.png)\n\n### 1.2，IEEE FP 和 BFP 格式\n\n![IEEE FP and Brain float formats](../images/ai_chips/ieee_fp_bfp.png)\n\n\n\n## 参考资料\n\n1. [Cloud TPU 系统架构](https://cloud.google.com/tpu/docs/system-architecture-tpu-vm?hl=zh-cn)\n2. https://dl.acm.org/doi/pdf/10.1145/3360307"
  },
  {
    "path": "6-model_deploy/ONNX模型分析与使用.md",
    "content": "> 本文大部分内容为对 ONNX 官方资料的总结和翻译，部分知识点参考网上质量高的博客。\n\n## 一，ONNX 概述\n\n深度学习算法大多通过计算数据流图来完成神经网络的深度学习过程。 一些框架（例如CNTK，Caffe2，Theano和TensorFlow）使用**静态图形**，而其他框架（例如 PyTorch 和 Chainer）使用**动态图形**。 但是这些框架都提供了接口，使开发人员可以轻松构建计算图和运行时，以优化的方式处理图。 这些图用作中间表示（IR），捕获开发人员源代码的特定意图，有助于优化和转换在特定设备（CPU，GPU，FPGA等）上运行。\n\n**ONNX 的本质只是一套开放的 `ML` 模型标准，模型文件存储的只是网络的拓扑结构和权重（其实每个深度学习框架最后保存的模型都是类似的），脱离开框架是没办法对模型直接进行 `inference`的**。\n\n### 1.1，为什么使用通用 IR\n现在很多的深度学习框架提供的功能都是类似的，但是在 API、计算图和 runtime 方面却是独立的，这就给 AI 开发者在不同平台部署不同模型带来了很多困难和挑战，ONNX 的目的在于提供一个跨框架的模型中间表达框架，用于模型转换和部署。ONNX 提供的计算图是通用的，格式也是开源的。\n\n## 二，ONNX 规范\n> Open Neural Network Exchange Intermediate Representation (ONNX IR) Specification.\n\n`ONNX` 结构的定义文件 `.proto` 和 `.prpto3` 可以在 [onnx folder](https://github.com/onnx/onnx/tree/master/onnx) 目录下找到，文件遵循的是谷歌 `Protobuf` 协议。ONNX 是一个开放式规范，由以下组件组成：\n+ 可扩展计算图模型的定义\n+ 标准数据类型的定义\n+ 内置运算符的定义\n\n`IR6` 版本的 ONNX 只能用于推理（inference），从 `IR7` 开始 ONNX 支持训练（training）。`onnx.proto` 主要的对象如下：\n+ ModelProto\n+ GraphProto\n+ NodeProto\n+ AttributeProto\n+ ValueInfoProto\n+ TensorProto\n\n他们之间的关系：ONNX 模型 `load` 之后，得到的是一个 `ModelProto`，它包含了一些版本信息，生产者信息和一个非常重要的 `GraphProto`；在 `GraphProto` 中包含了四个关键的 `repeated` 数组，分别是`node` (NodeProto 类型)，`input`(ValueInfoProto 类型)，`output`(ValueInfoProto 类型)和 `initializer` (TensorProto 类型)，其中 `node` 中存放着模型中的所有计算节点，input 中存放着模型所有的输入节点，output 存放着模型所有的输出节点，`initializer` 存放着模型所有的权重；节点与节点之间的拓扑定义可以通过 input 和output 这两个 `string` 数组的指向关系得到，这样利用上述信息我们可以快速构建出一个深度学习模型的拓扑图。最后每个计算节点当中还包含了一个 `AttributeProto` 数组，用于描述该节点的属性，例如 `Conv` 层的属性包含 `group`，`pads` 和`strides` 等等，具体每个计算节点的属性、输入和输出可以参考这个 [Operators.md](https://github.com/onnx/onnx/blob/master/docs/Operators.md) 文档。\n\n需要注意的是，上面所说的 `GraphProto` 中的 `input` 输入数组不仅仅包含我们一般理解中的图片输入的那个节点，还包含了模型当中所有权重。举例，`Conv` 层中的 `W` 权重实体是保存在 `initializer` 当中的，那么相应的会有一个同名的输入在 `input` 当中，其背后的逻辑应该是把权重也看作是模型的输入，并通过 `initializer` 中的权重实体来对这个输入做初始化(也就是把值填充进来)\n\n### 2.1，Model\n\n模型结构的主要目的是将元数据( `meta data`)与图形(`graph`)相关联，图形包含所有可执行元素。 首先，读取模型文件时使用元数据，为实现提供所需的信息，以确定它是否能够：执行模型，生成日志消息，错误报告等功能。此外元数据对工具很有用，例如IDE和模型库，它需要它来告知用户给定模型的目的和特征。\n\n每个 model 有以下组件：\n|Name|Type|Description|\n|---|---|---|\n|ir_version|int64|The ONNX version assumed by the model.|\n|opset_import|OperatorSetId|A collection of operator set identifiers made available to the model. An implementation must support all operators in the set or reject the model.|\n|producer_name|string|The name of the tool used to generate the model.|\n|producer_version|string|The version of the generating tool.|\n|domain|string|A reverse-DNS name to indicate the model namespace or domain, for example, 'org.onnx'|\n|model_version|int64|The version of the model itself, encoded in an integer.|\n|doc_string|string|Human-readable documentation for this model. Markdown is allowed.|\n|graph|Graph|The parameterized graph that is evaluated to execute the model.|\n|metadata_props|map<string,string>|Named metadata values; keys should be distinct.|\n|training_info|TrainingInfoProto[]|An optional extension that contains information for training.|\n\n### 2.2，Operators Sets\n\n每个模型必须明确命名它依赖于其功能的运算符集。 操作员集定义可用的操作符，其版本和状态。 每个模型按其域定义导入的运算符集。 所有模型都隐式导入默认的 ONNX 运算符集。\n\n运算符集(`Operators Sets`)对象的属性如下：\n|Name|Type|Description|\n|---|---|---|\n|magic|string|T ‘ONNXOPSET’|\n|ir_version|int32|The ONNX version corresponding to the operators.|\n|ir_version_prerelease|string|The prerelease component of the SemVer of the IR.\n|ir_build_metadata|string|The build metadata of this version of the operator set.|\n|domain|string|The domain of the operator set. Must be unique among all sets.|\n|opset_version|int64|The version of the operator set.|\n|doc_string|string|Human-readable documentation for this operator set. Markdown is allowed.|\n|operator|Operator[]|The operators contained in this operator set.|\n\n### 2.3，ONNX Operator\n\n图( `graph`)中使用的每个运算符必须由模型(`model`)导入的一个运算符集明确声明。\n\n运算符（`Operator`）对象定义的属性如下：\n|Name|Type|Description|\n|---|---|---|\n|op_type|string|The name of the operator, as used in graph nodes. MUST be unique within the operator set’s domain.|\n|since_version|int64|The version of the operator set when this operator was introduced.|\n|status|OperatorStatus|One of ‘EXPERIMENTAL’ or ‘STABLE.’|\n|doc_string|string|A human-readable documentation string for this operator. Markdown is allowed.|\n\n### 2.4，ONNX Graph\n\n序列化图由一组元数据字段(`metadata`)，模型参数列表(`a list of model parameters`,)和计算节点列表组成(a list of computation nodes)。每个计算数据流图被构造为拓扑排序的节点列表，这些节点形成图形，其必须没有周期。 每个节点代表对运营商的呼叫。 每个节点具有零个或多个输入以及一个或多个输出。\n\n图表(Graph)对象具有以下属性：\n\n|Name|Type|Description|\n|---|---|---|\n|name|string|模型计算图的名称|\n|node|Node[]|节点列表，基于输入/输出数据依存关系形成部分排序的计算图，拓扑顺序排列。|\n|initializer|Tensor[]|命名张量值的列表。 当 ` initializer` 与计算图 `graph`输入名称相同，输入指定一个默认值，否则指定一个常量值。|\n|doc_string|string|用于阅读模型的文档|\n|input|ValueInfo[]|计算图 `graph` 的输入参数，在 `‘initializer.’` 中可能能找到默认的初始化值。|\n|output|ValueInfo[]|计算图 `graph` 的输出参数。|\n|value_info|ValueInfo[]|用于存储除输入、输出值之外的类型和形状信息。|\n\n### 2.5，ValueInfo\n\n`ValueInfo` 对象属性如下：\n\n|Name|Type|Description|\n|---|---|---|\n|name|string|The name of the value/parameter.|\n|type|Type|The type of the value **including shape information**.|\n|doc_string|string|Human-readable documentation for this value. Markdown is allowed.|\n### 2.6，Standard data types\nONNX 标准有两个版本，主要区别在于支持的数据类型和算子不同。计算图 `graphs`、节点 `nodes`和计算图的 `initializers` 支持的数据类型如下。原始数字，字符串和布尔类型必须用作张量的元素。\n#### 2.6.1，Tensor Element Types\n|Group|Types|Description|\n|---|---|---|\n|Floating Point Types|float16, float32, float64|浮点数遵循IEEE 754-2008标准。|\n|Signed Integer Types|int8, int16, int32, int64|支持 `8-64` 位宽的有符号整数。|\n|Unsigned Integer Types|uint8, uint16|支持 `8` 或 `16` 位的无符号整数。|\n|Complex Types|complex64, complex128|具有 `32` 位或 `64` 位实部和虚部的复数。|\n|Other|string|字符串代表的文本数据。 所有字符串均使用UTF-8编码。|\n|Other|bool|布尔值类型，表示的数据只有两个值，通常为 `true` 和 `false`。|\n#### 2.6.2，Input / Output Data Types\n以下类型用于定义计算图和节点输入和输出的类型。\n\n|Variant | Type | Description |\n|---|---|---|\n|ONNX|dense tensors|张量是向量和矩阵的一般化|\n|ONNX|sequence| `sequence` (序列)是有序的稠密元素集合。|\n|ONNX|map|映射是关联表，由键类型和值类型定义。|\n\n**`ONNX` 现阶段没有定义稀疏张量类型**。\n## 三，ONNX版本控制\n\n## 四，主要算子概述\n\n## 五，Python API 使用\n\n### 5.1，加载模型\n\n1，**Loading an ONNX model**\n\n```python\nimport onnx\n# onnx_model is an in-mempry ModelProto\nonnx_model = onnx.load('path/to/the/model.onnx') # 加载 onnx 模型\n```\n\n2，**Loading an ONNX Model with External Data**\n\n+ 【默认加载模型方式】如果外部数据(`external data`)和模型文件在同一个目录下，仅使用 `onnx.load()` 即可加载模型，方法见上小节。\n+ 如果外部数据(`external data`)和模型文件不在同一个目录下，在使用 `onnx_load()` 函数后还需使用 `load_external_data_for_model()` 函数指定外部数据路径。\n\n```python\nimport onnx\nfrom onnx.external_data_helper import load_external_data_for_model\n\nonnx_model = onnx.load('path/to/the/model.onnx', load_external_data=False)\nload_external_data_for_model(onnx_model, 'data/directory/path/')\n# Then the onnx_model has loaded the external data from the specific directory\n```\n\n**3，Converting an ONNX Model to External Data**\n\n```python\nfrom onnx.external_data_helper import convert_model_to_external_data\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\nconvert_model_to_external_data(onnx_model, all_tensors_to_one_file=True, location='filename', size_threshold=1024, convert_attribute=False)\n# Then the onnx_model has converted raw data as external data\n# Must be followed by save\n```\n\n### 5.2，保存模型\n\n**1，Saving an ONNX Model**\n```python\nimport onnx\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\n\n# Save the ONNX model\nonnx.save(onnx_model, 'path/to/the/model.onnx')\n```\n2，**Converting and Saving an ONNX Model to External Data**\n```python\nimport onnx\n\n# onnx_model is an in-memory ModelProto\nonnx_model = ...\nonnx.save_model(onnx_model, 'path/to/save/the/model.onnx', save_as_external_data=True, all_tensors_to_one_file=True, location='filename', size_threshold=1024, convert_attribute=False)\n# Then the onnx_model has converted raw data as external data and saved to specific directory\n```\n### 5.3，Manipulating TensorProto and Numpy Array\n```python\nimport numpy\nimport onnx\nfrom onnx import numpy_helper\n\n# Preprocessing: create a Numpy array\nnumpy_array = numpy.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=float)\nprint('Original Numpy array:\\n{}\\n'.format(numpy_array))\n\n# Convert the Numpy array to a TensorProto\ntensor = numpy_helper.from_array(numpy_array)\nprint('TensorProto:\\n{}'.format(tensor))\n\n# Convert the TensorProto to a Numpy array\nnew_array = numpy_helper.to_array(tensor)\nprint('After round trip, Numpy array:\\n{}\\n'.format(new_array))\n\n# Save the TensorProto\nwith open('tensor.pb', 'wb') as f:\n    f.write(tensor.SerializeToString())\n\n# Load a TensorProto\nnew_tensor = onnx.TensorProto()\nwith open('tensor.pb', 'rb') as f:\n    new_tensor.ParseFromString(f.read())\nprint('After saving and loading, new TensorProto:\\n{}'.format(new_tensor))\n```\n### 5.4，创建ONNX模型\n可以通过 `helper` 模块提供的函数 `helper.make_graph` 完成创建 ONNX 格式的模型。创建 `graph` 之前，需要先创建相应的 `NodeProto(node)`，参照文档设定节点的属性，指定该节点的输入与输出，如果该节点带有权重那还需要创建相应的`ValueInfoProto` 和 `TensorProto` 分别放入 `graph` 中的 `input` 和 `initializer` 中，以上步骤缺一不可。\n```python\nimport onnx\nfrom onnx import helper\nfrom onnx import AttributeProto, TensorProto, GraphProto\n\n\n# The protobuf definition can be found here:\n# https://github.com/onnx/onnx/blob/master/onnx/onnx.proto\n\n# Create one input (ValueInfoProto)\nX = helper.make_tensor_value_info('X', TensorProto.FLOAT, [3, 2])\npads = helper.make_tensor_value_info('pads', TensorProto.FLOAT, [1, 4])\n\nvalue = helper.make_tensor_value_info('value', AttributeProto.FLOAT, [1])\n\n# Create one output (ValueInfoProto)\nY = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [3, 4])\n\n# Create a node (NodeProto) - This is based on Pad-11\nnode_def = helper.make_node(\n    'Pad',                  # name\n    ['X', 'pads', 'value'], # inputs\n    ['Y'],                  # outputs\n    mode='constant',        # attributes\n)\n\n# Create the graph (GraphProto)\ngraph_def = helper.make_graph(\n    [node_def],        # nodes\n    'test-model',      # name\n    [X, pads, value],  # inputs\n    [Y],               # outputs\n)\n\n# Create the model (ModelProto)\nmodel_def = helper.make_model(graph_def, producer_name='onnx-example')\n\nprint('The model is:\\n{}'.format(model_def))\nonnx.checker.check_model(model_def)\nprint('The model is checked!')\n```\n### 5.5，检查模型\n在完成 `ONNX` 模型加载或者创建后，有必要对模型进行检查，使用 `onnx.check.check_model()` 函数。\n```python\nimport onnx\n\n# Preprocessing: load the ONNX model\nmodel_path = 'path/to/the/model.onnx'\nonnx_model = onnx.load(model_path)\n\nprint('The model is:\\n{}'.format(onnx_model))\n\n# Check the model\ntry:\n    onnx.checker.check_model(onnx_model)\nexcept onnx.checker.ValidationError as e:\n    print('The model is invalid: %s' % e)\nelse:\n    print('The model is valid!')\n```\n### 5.6，实用功能函数\n函数 `extract_model()` 可以从 `ONNX` 模型中提取子模型，子模型由输入和输出张量的名称定义。这个功能方便我们 `debug` 原模型和转换后的 `ONNX` 模型输出结果是否一致(误差小于某个阈值)，不再需要我们手动去修改 `ONNX` 模型。\n```python\nimport onnx\n\ninput_path = 'path/to/the/original/model.onnx'\noutput_path = 'path/to/save/the/extracted/model.onnx'\ninput_names = ['input_0', 'input_1', 'input_2']\noutput_names = ['output_0', 'output_1']\n\nonnx.utils.extract_model(input_path, output_path, input_names, output_names)\n```\n### 5.7，工具\n函数 `update_inputs_outputs_dims()` 可以将模型输入和输出的维度更新为参数中指定的值，可以使用 `dim_param` 提供静态和动态尺寸大小。\n```python\nimport onnx\nfrom onnx.tools import update_model_dims\n\nmodel = onnx.load('path/to/the/model.onnx')\n# Here both 'seq', 'batch' and -1 are dynamic using dim_param.\nvariable_length_model = update_model_dims.update_inputs_outputs_dims(model, {'input_name': ['seq', 'batch', 3, -1]}, {'output_name': ['seq', 'batch', 1, -1]})\n# need to check model after the input/output sizes are updated\nonnx.checker.check_model(variable_length_model )\n```\n## 参考资料\n1. [ONNX--跨框架的模型中间表达框架](https://zhuanlan.zhihu.com/p/41255090)\n2. [深度学习模型转换与部署那些事(含ONNX格式详细分析)](https://bindog.github.io/blog/2020/03/13/deep-learning-model-convert-and-depoly/)\n3. [onnx](https://github.com/onnx/tutorials)"
  },
  {
    "path": "6-model_deploy/TensorRT基础笔记.md",
    "content": "## 一，概述\n\n`TensorRT` 是 NVIDIA 官方推出的基于 `CUDA` 和 `cudnn` 的高性能深度学习推理加速引擎，能够使深度学习模型在 `GPU` 上进行低延迟、高吞吐量的部署。采用 `C++` 开发，并提供了 `C++` 和 `Python` 的 API 接口，支持 TensorFlow、Pytorch、Caffe、Mxnet 等深度学习框架，其中 `Mxnet`、`Pytorch` 的支持需要先转换为中间模型 `ONNX` 格式。截止到 2021.4.21 日， `TensorRT` 最新版本为 `v7.2.3.4`。\n\n深度学习领域**延迟和吞吐量**的一般解释：\n\n+ 延迟 (`Latency`): 人和机器做决策或采取行动时都需要反应时间。延迟是指**提出请求与收到反应之间经过的时间**。大部分人性化软件系统（不只是 AI 系统），延迟都是以**毫秒**来计量的。\n+ 吞吐量 (`Throughput`): 在给定创建或部署的深度学习网络规模的情况下，可以传递多少推断结果。简单理解就是在**一个时间单元（如：一秒）内网络能处理的最大输入样例数**。\n\n## 二，TensorRT 工作流程\n\n在描述 `TensorRT` 的优化原理之前，需要先了解 `TensorRT` 的工作流程。首先输入一个训练好的 `FP32` 模型文件，并通过 `parser` 等方式输入到 `TensorRT` 中做解析，解析完成后 `engin` 会进行计算图优化（优化原理在下一章）。得到优化好的 `engine` 可以序列化到内存（`buffer`）或文件（`file`），读的时候需要反序列化，将其变成 `engine`以供使用。然后在执行的时候创建 `context`，主要是分配预先的资源，`engine` 加 `context` 就可以做推理（`Inference`）。\n\n![TensorRT工作流程.jpg](../images/TensorRT/tensorrt_workflow.jpg)\n\n## 三，TensorRT 的优化原理\n\n`TensorRT` 的优化主要有以下几点：\n\n1. **算子融合（网络层合并）**：我们知道 `GPU` 上跑的函数叫 `Kernel`，`TensorRT` 是存在 `Kernel` 调用的，频繁的 `Kernel` 调用会带来性能开销，主要体现在：数据流图的调度开销，GPU内核函数的启动开销，以及内核函数之间的数据传输开销。大多数网络中存在连续的卷积 `conv` 层、偏置 `bias` 层和 激活 `relu` 层，这三层需要调用三次 cuDNN 对应的 API，但实际上这三个算子是可以进行融合（合并）的，合并成一个 `CBR` 结构。同时目前的网络一方面越来越深，另一方面越来越宽，可能并行做若干个相同大小的卷积，这些卷积计算其实也是可以合并到一起来做的（横向融合）。比如 `GoogLeNet` 网络，把结构相同，但是权值不同的层合并成一个更宽的层。\n2. `concat` 层的消除。对于 `channel` 维度的 `concat` 层，`TensorRT` 通过非拷贝方式将层输出定向到正确的内存地址来消除 `concat` 层，从而减少内存访存次数。\n3. `Kernel` 可以根据不同 `batch size` 大小和问题的复杂度，去自动选择最合适的算法，`TensorRT` 预先写了很多 `GPU` 实现，有一个自动选择的过程（没找到资料理解）。其问题包括：怎么调用 `CUDA` 核心、怎么分配、每个 `block` 里面分配多少个线程、每个 `grid` 里面有多少个 `block`。\n\n4. `FP32->FP16、INT8、INT4`：低精度量化，模型体积更小、内存占用和延迟更低等。\n5. 不同的硬件如 `P4` 卡还是 `V100` 卡甚至是嵌入式设备的卡，`TensorRT` 都会做对应的优化，得到优化后的 `engine`。\n\n## 四，参考资料\n\n1. [内核融合：GPU深度学习的“加速神器”](https://www.msra.cn/zh-cn/news/features/kernel-fusion-20170925)\n2. [高性能深度学习支持引擎实战——TensorRT](https://zhuanlan.zhihu.com/p/35657027)\n3. 《NVIDIA TensorRT 以及实战记录》PPT\n4. https://www.tiriasresearch.com/wp-content/uploads/2018/05/TIRIAS-Research-NVIDIA-PLASTER-Deep-Learning-Framework.pdf\n"
  },
  {
    "path": "6-model_deploy/ncnn源码解析-Net类.md",
    "content": "## 一，网络类 Net\n\n`net.cpp/net.h` 是模型结构定义和模型推理类所在文件，主要包括以下类：\n\n- `Net`: 网络类，主要提供网络结构和网络权重文件加载/解析接口: `load_param` 和 `load_model`。\n- `Extractor`: 模型推理之行类，主要对外接口是网络输入函数 `input` 和网络推理函数 `extract`。\n- `NetPrivate`：Net 类的私有成员都定义这个单独的类中，比如 blobs、layers、input_blob_indexes、output_blob_indexes、custom_layer_registry 和 `local_blob_allocator` 等。\n- `ExtractorPrivate`: Extractor 类的私有成员都定义这个单独的类中。\n\n将 `ncnn::Net` 类的私有成员封装成了一个类 `NetPrivate`，ncnn 框架中很多类都有类似操作，比如 `Pipeline`、`ParamDict`、`Extractor`、`PoolAllocator`、`DataReaderFromMemory` 等类。\n\n### 1.1，Net 类解析\n\n`Net` 的部分定义如下（省略了部分代码）:\n\n```cpp\nclass Net\n{\npublic:\n    // empty init\n    Net();\n    // clear and destroy\n    virtual ~Net();\n\npublic:\n    // option can be changed before loading\n    Option opt;\n    int register_custom_layer(int index, layer_creator_func creator, \t\t      layer_destroyer_func destroyer = 0, void* userdata = 0);\n    // 实际的模型结构和权重文件加载函数\n    int load_param_bin(const DataReader& dr);\n    int load_model(const DataReader& dr);\n    \n    // load network structure from binary param file\n  \tint load_param(const char* protopath);\n    // load network weight data from model file return 0 if success\n    int load_model(const char* modelpath);\n\nprivate:\n    NetPrivate* const d; // 类\n};\n```\n\n`NetPrivate` 主要成员变量是：\n\n```cpp\n// Blob 用于记录 featuremap 张量数据\nstd::vector<Blob> blobs;\nstd::vector<Layer*> layers;\n\nstd::vector<int> input_blob_indexes;\nstd::vector<int> output_blob_indexes;\n```\n\n## 二，参数字典类 paramdict\n\nNet::load_param() 函数中用到 ParamDict 类的代码有以下三处，\n\n```cpp\nMat shape_hints = pd.get(30, Mat());\nlayer->featmask = pd.get(31, 0);\nint lr = layer->load_param(pd);\n```\n\nParamDict 类中是通过 ParamDictPrivate 类保存私有成员变量，ParamDict 类定义如下所示:\n\n```cpp\nclass NCNN_EXPORT ParamDict\n{\npublic:\n    // empty\n    ParamDict();\n\n    virtual ~ParamDict();\n\n    // copy\n    ParamDict(const ParamDict&);\n\n    // assign\n    ParamDict& operator=(const ParamDict&);\n\n    // get type\n    int type(int id) const;\n\n    // get int\n    int get(int id, int def) const;\n    // get float\n    float get(int id, float def) const;\n    // get array\n    Mat get(int id, const Mat& def) const;\n\n    // set int\n    void set(int id, int i);\n    // set float\n    void set(int id, float f);\n    // set array\n    void set(int id, const Mat& v);\n\nprotected:\n    friend class Net;\n\n    void clear();\n\n    int load_param(const DataReader& dr);\n    int load_param_bin(const DataReader& dr);\n\nprivate:\n    ParamDictPrivate* const d;\n};\n```\n\nParamDict 类的主要成员函数 load_param 解析如下:\n\n```cpp\nint ParamDict::load_param(const DataReader& dr)\n{\n    clear();\n\n    //     0=100 1=1.250000 -23303=5,0.1,0.2,0.4,0.8,1.0\n\n    // parse each key=value pair\n    int id = 0;\n    while (dr.scan(\"%d=\", &id) == 1)\n    {\n        // 是否为数组类型\n        bool is_array = id <= -23300;\n        if (is_array)\n        {\n            id = -id - 23300;\n        }\n        // id 是否超过最大参数数\n        if (id >= NCNN_MAX_PARAM_COUNT)\n        {\n            NCNN_LOGE(\"id < NCNN_MAX_PARAM_COUNT failed (id=%d, NCNN_MAX_PARAM_COUNT=%d)\", id, NCNN_MAX_PARAM_COUNT);\n            return -1;\n        }\n        // 如果是数组类型，执行以下解析操作\n        if (is_array)\n        {\n            int len = 0;\n            int nscan = dr.scan(\"%d\", &len); // 解析数组长度\n            if (nscan != 1)\n            {\n                NCNN_LOGE(\"ParamDict read array length failed\");\n                return -1;\n            }\n\n            d->params[id].v.create(len); // 创建数组\n            // 遍历数组元素并解析\n            for (int j = 0; j < len; j++)\n            {   \n                // 解析数组元素\n                char vstr[16];\n                nscan = dr.scan(\",%15[^,\\n ]\", vstr);\n                if (nscan != 1)\n                {\n                    NCNN_LOGE(\"ParamDict read array element failed\");\n                    return -1;\n                }\n                \n                // 是否为浮点数，看解析的字符串中是否存在'.'或'e'\n                // 小数点计数法和科学计数法\n                bool is_float = vstr_is_float(vstr);\n\n                // 转换为相应类型\n                if (is_float)\n                {\n                    float* ptr = d->params[id].v;\n                    ptr[j] = vstr_to_float(vstr);\n                }\n                else\n                {\n                  \t// vstr赋值给params[id].v[j]\n                    int* ptr = d->params[id].v;\n                    nscan = sscanf(vstr, \"%d\", &ptr[j]);\n                    if (nscan != 1)\n                    {\n                        NCNN_LOGE(\"ParamDict parse array element failed\");\n                        return -1;\n                    }\n                }\n                // 设置参数类型\n                d->params[id].type = is_float ? 6 : 5;\n            }\n        }\n        // 如果不是数组类型，则解析单个值，步骤和 if 内部语句快一样\n        else\n        {\n            char vstr[16];\n            int nscan = dr.scan(\"%15s\", vstr);\n            if (nscan != 1)\n            {\n                NCNN_LOGE(\"ParamDict read value failed\");\n                return -1;\n            }\n\n            bool is_float = vstr_is_float(vstr);\n\n            if (is_float)\n            {\n                d->params[id].f = vstr_to_float(vstr);\n            }\n            else\n            {\n                nscan = sscanf(vstr, \"%d\", &d->params[id].i);\n                if (nscan != 1)\n                {\n                    NCNN_LOGE(\"ParamDict parse value failed\");\n                    return -1;\n                }\n            }\n\n            d->params[id].type = is_float ? 3 : 2;\n        }\n    }\n\n    return 0;\n}\n#endif // NCNN_STRING\n```\n\ndr.scan 对应的函数是 DataReaderFromMemory::scan，其定义如下所示:\n\n```cpp\n#if NCNN_STRING // 判断是否定义了 NCNN_STRING 宏\nint DataReaderFromMemory::scan(const char* format, void* p) const\n{\n    // 获取给定格式字符串的长度\n    size_t fmtlen = strlen(format);\n\n    // 在原格式字符串后添加 '%n'，用于返回已经读取的字符数\n    char* format_with_n = new char[fmtlen + 4];\n    sprintf(format_with_n, \"%s%%n\", format);\n\n    int nconsumed = 0; // 记录已经读取的字符数\n    int nscan = sscanf((const char*)d->mem, format_with_n, p, &nconsumed); // 读取数据\n    d->mem += nconsumed; // 更新指针，指向未读取的内存\n\n    delete[] format_with_n; // 释放动态分配的内存\n\n    return nconsumed > 0 ? nscan : 0; // 返回已经读取的字符数或者 0\n}\n#endif // NCNN_STRING\n\n```\n\nDataReaderFromMemory::scan 函数主要实现了**从内存中读取数据并按照给定的格式进行解析**。该函数首先获取给定格式字符串的长度，并在该字符串后添加 `%n`，用于返回已经读取的字符数。然后通过 `sscanf` 函数读取数据并更新指向未读取的内存的指针，最后返回已经读取的字符数或者 0。\n\n值得注意的是，该函数代码依赖于 `NCNN_STRING` 宏，只有在定义了该宏时才会编译。\n\nParamDictPrivate 类定义如下所示:\n\n```cpp\n#define NCNN_MAX_PARAM_COUNT 32\nclass ParamDictPrivate\n{\npublic:\n    struct\n    {\n        // 0 = null\n        // 1 = int/float\n        // 2 = int\n        // 3 = float\n        // 4 = array of int/float\n        // 5 = array of int\n        // 6 = array of float\n        int type;\n        union\n        {\n            int i;\n            float f;\n        };\n        Mat v;\n    } params[NCNN_MAX_PARAM_COUNT];\n};\n```\n\nNCNN_MAX_PARAM_COUNT 被宏定义为 32，表示 params 是一个大小为 32 的结构体数组，即模型参数文件每一行中特定参数数量不能超过 32。类中结构体的作用是存储参数值，可以根据 `type` 的值来确定参数类型。\n"
  },
  {
    "path": "6-model_deploy/ncnn源码解析-sample运行.md",
    "content": "- [一，依赖库知识速学](#一依赖库知识速学)\n  - [aarch64](#aarch64)\n  - [OpenMP](#openmp)\n  - [AVX512](#avx512)\n  - [submodule](#submodule)\n  - [apt upgrade](#apt-upgrade)\n- [二，硬件基础知识速学](#二硬件基础知识速学)\n  - [2.1，内存](#21内存)\n  - [2.2，CPU](#22cpu)\n- [三，ncnn 推理模型](#三ncnn-推理模型)\n  - [3.1，shufflenetv2 模型推理解析](#31shufflenetv2-模型推理解析)\n  - [3.2，网络推理过程解析](#32网络推理过程解析)\n  - [3.3，模型推理过程总结](#33模型推理过程总结)\n- [参考资料](#参考资料)\n\n## 一，依赖库知识速学\n\n### aarch64\n\n`aarch64`，也被称为 ARM64，是一种基于 `ARMv8-A` 架构的 `64` 位指令集体系结构。它是 ARM 体系结构的最新版本，旨在提供更好的性能和能效比。与先前的 `32` 位 `ARM` 架构相比，aarch64 具有更大的寻址空间、更多的寄存器和更好的浮点性能。\n\n在 Linux 系统终端下输入以下命令，查看 `cpu` 架构。\n\n```bash\nuname -m # 我的英特尔服务器输出 x86_64，m1 pro 苹果电脑输出 arm64\n```\n### OpenMP\n\n`OpenMP`（Open Multi-Processing）是一种基于共享内存的并行编程 API，用于编写多线程并行程序。使用 `OpenMP`，程序员可以通过在程序中**插入指令**来指示程序中的并行性。这些指令是以 `#pragma` 开头的编译指示符，告诉编译器如何并行化代码。\n\n```cpp\n#include <stdio.h>\n#include <omp.h>\n\nint main() {\n    int i;\n    #pragma omp parallel for\n    for(i = 0; i < 10; i++) {\n        printf(\"Thread %d executing iteration %d\\n\", omp_get_thread_num(), i);\n    }\n    return 0;\n}\n```\n\n### AVX512\n\n`AVX` 全称是 Advanced Vector Extension，高级矢量扩展，用于处理 `N` 维数据的，例如 `8` 维及以下的 `64` 位双精度浮点矢量或 `16` 维及以下的单精度浮点矢量。\n\n`AVX512` 是 `SIMD` 指令（单指令多数据），`x86` 架构上最早的 SIMD 指令是 128bit 的 `SSE`，然后是 256bit 的 AVX/AVX2，最后是现在 512bit 的 AVX512。\n\n### submodule\n\ngithub submodule（子模块）允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中，同时还保持提交的独立。\n\n### apt upgrade\n\n- `apt update`：只检查，不更新（已安装的软件包是否有可用的更新，给出汇总报告）。\n- `apt upgrade`：更新已安装的软件包。\n\n## 二，硬件基础知识速学\n### 2.1，内存\n\n`RAM`（随机访问存储）的一些关键特性是带宽(`bandwidth`)和延迟(`latency`)。\n\n### 2.2，CPU\n\n中央处理器(central processing unit，`CPU`)是任何计算机的核心，其由许多关键组件组成:\n- **处理器核心** (processor cores): 用于执行机器代码的。\n- **总线**（bus）: 用于连接不同组件(注意，总线会因为处理器型号、 各代产品和供应商之间的特定拓扑结构有明显不同)\n- **缓存**(cache): 一般是三级缓（L1/L2/L3 cache），相比主内存实现更高的读取带宽和更低的延迟内存访问。\n\n现代 CPU 都包含向量处理单元，都提供了 `SIMD` 指令，可以在单个指令中同时处理多个数据，从而支持高性能线性代数和卷积运算。这些 `SIMD` 指令有不同的名称: 在 ARM 上叫做 NEON，在 x86 上被称 为AVX2156。\n\n一个典型的 Intel Skylake 消费级四核 CPU，其核心架构如下图所示。\n\n![cpu 核心架构](../images/ncnn/cpu_architecture.png)\n\n## 三，ncnn 推理模型\n\n### 3.1，shufflenetv2 模型推理解析\n\n这里以分类网络 shufflenetv2 为例，分析如何使用 `ncnn` 框架模型推理。源码在 `ncnn/examples/shufflenetv2.cpp`文件中，程序主要分为两个函数，分别是 `detect_shufflenetv2()` 和 `print_topk()`。前者用于运行图片分类网络，后者用于输出前 N 个分类结果。代码流程总结如下:\n\n1. 在 `detect_shufflenetv2` 函数中，主要使用了 `ncnn::Net` 类进行模型加载和推理，主要流程如下：\n   - 加载模型参数和模型二进制文件。\n   - 将输入图片 `cv::Mat` 格式转换为 `ncnn::Mat` 格式，同时进行 resize 和归一化操作。\n   - 创建 `ncnn::Extractor` 对象，并设置输入和输出。\n   - 进行推理计算，得到分类输出结果。\n   - 对输出结果进行 `softmax` 操作。\n   - 将输出结果转换为 vector<float> 类型的数据，存储到 cls_scores 中。\n\n2. 调用 `print_topk` 函数输出 cls_scores 的前 `topk` 个类别及其得分，具体实现步骤如下：\n   - 定义一个向量 `std::vector<std::pair<float, int>> vec`，其元素类型为 `<float, int>`，其中第一个元素为分类得分，第二个元素为该分类的索引。\n   - 遍历分类模型输出结果 `cls_scores`，将其与索引值组成一个 `<float, int>` 类型的元素，放入向量 `vec` 中。\n   - 使用 `std::partial_sort()` 函数，将向量 `vec` 进行部分排序，按照得分从大到小的顺序排列。\n   - 遍历排好序的向量 `vec`，输出前 `topk` 个元素的索引和得分值。\n\n3. 最后主函数 main 中先调用 cv::imread 函数完成图像的读取操作，而后调用 `detect_shufflenetv2` 和 `print_topk` 函数，完成 shufflenetv2 网络推理和图片分类结果概率值输出的操作。\n\n`print_topk` 函数代码及其注释如下:\n\n```cpp\n// 定义函数，输入为一个向量 cls_scores 和需要输出的 topk 数量\nstatic int print_topk(const std::vector<float>& cls_scores, int topk)\n{\n    // 1，定义一个向量 vec，其元素类型为 <float, int>，用于存储分类得分和索引值\n    int size = cls_scores.size();\n    std::vector<std::pair<float, int> > vec;\n    vec.resize(size);\n\n    // 2，遍历分类得分，将其与索引值组成 <float, int> 元素，并存入向量 vec 中\n    for (int i = 0; i < size; i++)\n    {\n        vec[i] = std::make_pair(cls_scores[i], i);\n    }\n\n    // 3，使用 std::partial_sort() 函数，将向量 vec 进行部分排序，按照得分从大到小的顺序排列\n    std::partial_sort(vec.begin(), vec.begin() + topk, vec.end(),\n                      std::greater<std::pair<float, int> >());\n\n    // 4，遍历排好序的向量 vec，输出前 topk 个元素的索引和得分值\n    for (int i = 0; i < topk; i++)\n    {\n        float score = vec[i].first;\n        int index = vec[i].second;\n        fprintf(stderr, \"%d = %f\\n\", index, score);\n    }\n\n    return 0;\n}\n```\n\n值得注意的是，虽然调用 `print_topk` 函数得到了最高得分及其类别索引，但还需要**将类别索引转换为类别字符串**。这通常需要预先定义一个包含所有类别字符串的向量 `class_names`，并将其与类别索引一一对应。另外， `class_names` 的定义需与模型训练时的类别标签一致，否则会出现类别不匹配的情况。\n\n最后，实际跑下 sample 看下运行结果，这里模型用的是 imagenet 训练的 shufflenetv2 模型，然后用编译好的 shufflenetv2 程序去跑测试图片，输入图片和程序运行结果如下:\n\n![dog](../images/ncnn/dog.png)\n\n```bash\n/ncnn/build/examples# ./shufflenetv2 demo.jpeg\n270 = 0.455700\n279 = 0.303561\n174 = 0.057936\n```\n\n输入图像的类别索引是 `270`，参考文章[ImageNet 2012 1000分类名称和编号](https://zhuanlan.zhihu.com/p/315368462)，可知该类别是 dog（狗）。\n\n### 3.2，网络推理过程解析\n\n下面再看下**网络推理**代码的整体流程解析：\n\n1，首先需要 `Net` 对象，然后使用 `load_param` 和 `load_bin` 两个接口载入模型结构参数和模型权重参数文件:\n\n```cpp\n// 为了方便阅读，和官方代码比有所删减\nncnn::Net shufflenetv2;\nshufflenetv2.load_param(\"shufflenet_v2_x0.5.param\")\nshufflenetv2.load_model(\"shufflenet_v2_x0.5.bin\")\n```\n\n2，定义好 Net 对象后，可以调用相应的 create_extractor 接口创建 `Extractor`，Extractor 对象是完成图像数据输入和模型推理的类，虽然它也是对 Net 的相关接口做了封装。\n\n```cpp\nncnn::Extractor ex = shufflenetv2.create_extractor();\nex.input(\"data\", in);\nncnn::Mat out;\nex.extract(\"fc\", out); // 提取网络输出结果到 out 矩阵中\n```\n\n3，模型推理结果后处理，对网络推理结果执行 softmax 操作得到概率矩阵，而后转换为 vector<float> 类型的数据。\n\n```cpp\n// 对输出结果矩阵进行 softmax 操作\n// manually call softmax on the fc output\n// convert result into probability\n// skip if your model already has softmax operation\n{\n    ncnn::Layer* softmax = ncnn::create_layer(\"Softmax\");\n\n    ncnn::ParamDict pd;\n    softmax->load_param(pd);\n\n    softmax->forward_inplace(out, shufflenetv2.opt);\n\n    delete softmax;\n}\n\n// 将softmax输出结果转换为 vector<float> 类型的数据，存储到 cls_scores 中\nout = out.reshape(out.w * out.h * out.c);\n\ncls_scores.resize(out.w);\nfor (int j = 0; j < out.w; j++)\n{\n    cls_scores[j] = out[j];\n}\n```\n\n这里之所以需要手动调用 softmax 层，是因为官方提供的 shufflenetv2 模型结构文件的最后一层是 `fc` 层，没有 `softmax` 层。\n\n![shufflenetv2_param](../images/ncnn/shufflenetv2_param.png)\n\n值得注意的是，ncnn::Mat 类型默认采用的是 NCHW （通道在前，即 Number-Channel-Height-Width）的格式。在常见的分类任务中，ncnn 网络输出的一般是一个大小为 [1, 1, num_classes] 的张量，其中第三个维度的大小为类别数，上述代码即 `out.w` 表示类别数量，而 out.h 和 out.c 都为 1。\n\n### 3.3，模型推理过程总结\n\n1，模型推理过程可总结为下述步骤:\n\n1. **输入数据准备**：输入数据可以是图像、文本或其他形式的数据。在ncnn中，输入数据通常被转化为多维张量，其中第一维是数据的数量，其余维度表示数据的形状和尺寸。\n2. **加载模型参数和模型权重文件**：通过 Net 类的 `load_param` 和 `load_bin` 两个接口实现。\n3. **模型前向计算**：从模型的输入层开始，逐层计算模型的输出。每个层接收上一层的输出作为输入，并执行特定的算子，比如：卷积、池化、全连接等。在逐层计算过程中，模型各层的参数和权重数据也被用于更新模型的输出。最终，模型的输出被传递到模型的输出层。\n4. **输出数据解析**：模型的输出数据通常被转化为外部应用程序可用的格式。例如，在图像分类任务中，模型的输出可以是一个**概率向量**，表示输入图像属于每个类别的概率分布。在ncnn中，输出数据可以转化为多维张量或其他形式的数据。\n\n2，ncnn 加载/解析模型参数和权重文件的步骤还是很复杂的，可总结如下:\n\n1. 读取二进制参数和权重文件，并存储为字节数组。\n2. 解析字节数组中的头部信息，包括文件版本号、模型结构信息等。\n3. 解析层级信息，包括每个层的**名称、类型、输入输出维度**等信息，并保存在 `blobs` 中，Blob 类由：网络层 name、**依赖层索引**：producer 和 consumer，及上一层和下一网络层索引、**网络层 shape** 组成。\n4. 解析每个层的参数和权重数据，将其存储为矩阵或向量。\n\n## 参考资料\n\n1. [Git submodule使用指南（一）](https://juejin.cn/post/6844903812524670984)"
  },
  {
    "path": "6-model_deploy/卷积神经网络复杂度分析.md",
    "content": "- [前言](#前言)\n- [一 模型计算量分析](#一-模型计算量分析)\n  - [1.1 计算利用率(Utilization)](#11-计算利用率utilization)\n- [二 模型参数量计算](#二-模型参数量计算)\n  - [2.1 内存访问代价计算](#21-内存访问代价计算)\n- [三 模型计算量和参数量单位](#三-模型计算量和参数量单位)\n  - [3.1 浮点计算能力](#31-浮点计算能力)\n  - [3.2 双精度、单精度和半精度](#32-双精度单精度和半精度)\n  - [3.3 bfloat16 精度](#33-bfloat16-精度)\n  - [3.4 参数量/计算量分析工具](#34-参数量计算量分析工具)\n- [参考资料](#参考资料)\n\n## 前言\n\n现阶段的轻量级模型 MobileNet/ShuffleNet 系列、CSPNet、RepVGG、VoVNet 等都必须依赖于于具体的计算平台（如 CPU/GPU/ASIC 等）才能更完美的发挥网络架构。\n\n1，计算平台主要有两个指标：算力 $\\pi $和 带宽 $\\beta $。\n\n- **算力**：计算平台每秒完成的最大浮点运算次数，单位是 `FLOPS`\n- **带宽**：计算平台一次每秒最多能搬运多少数据（每秒能完成的内存交换量），单位是 `Byte/s`。\n\n计算强度上限 $I_{max}$，上面两个指标相除得到计算平台的**计算强度上限**。`AI` 是衡量从内存加载或存储的每个字节完成了多少操作\n\n$$I_{max} = \\frac {\\pi }{\\beta}$$\n\n> 这里所说的“内存”是广义上的内存。对于 `CPU` 而言指的就是真正的内存（`RAM`）；而对于 `GPU` 则指的是显存。\n\n2，和计算平台的两个指标相呼应，模型有两个反馈速度的**间接指标**：计算量 `FLOPs` 和访存量 `MAC`。\n\n- **计算量（FLOPs）**：指的是输入单个样本（一张图像），模型完成一次前向传播所发生的浮点运算次数，即模型的时间复杂度，单位是 `FLOPs`。\n- **访存量（MAC）**：指的是输入单个样本（一张图像），模型完成一次前向传播所发生的内存交换总量，即模型的空间复杂度，单位是 `Byte`，因为 CNN 模型的权重类型通常为 `float32`，所以一般需要乘以 `4`。`CNN` 网络中每个网络层 `MAC` 的计算分为读输入 `feature map` 大小、权重大小（`DDR` 读）和写输出 `feature map` 大小（`DDR` 写）三部分。\n- 模型的计算强度 $I$ ：$I = \\frac{FLOPs}{MAC}$，即计算量除以访存量后的值，**表示此模型在计算过程中，每 `Byte` 内存交换到底用于进行多少次浮点运算**。单位是 `FLOPs/Byte`。可以看到，模型计算强度越大，其内存使用效率越高。\n- 模型的理论性能 $P$ ：我们最关心的指标，即模型在计算平台上所能达到的每秒浮点运算次数（理论值）。单位是 `FLOPS or FLOP/s`。`Roof-line Model` 给出的就是计算这个指标的方法。\n\n## 一 模型计算量分析\n\n> 终端设备上运行深度学习算法需要考虑内存和算力的需求，因此需要进行模型复杂度分析，涉及到模型计算量（时间/计算复杂度）和模型参数量（空间复杂度）分析。\n\n为了分析模型计算复杂度，一个广泛采用的度量方式是模型推断时浮点运算的次数 （`FLOPs`），即模型理论计算量，但是，它是一个间接的度量，是对我们真正关心的直接度量比如速度或者时延的一种近似估计。\n\n本文的卷积核尺寸假设为为一般情况，即正方形，长宽相等都为 `K`。\n\n+ `FLOPs`：floating point operations 指的是浮点运算次数,**理解为计算量**，可以用来衡量算法/模型时间的复杂度。\n+ `FLOPS`：（全部大写）,Floating-point Operations Per Second，每秒所执行的浮点运算次数，理解为计算速度,是一个衡量硬件性能/模型速度的指标。\n+ `MACCs`：multiply-accumulate operations，乘-加操作次数，`MACCs` 大约是 FLOPs 的一半。将 $w[0]*x[0] + ...$ 视为一个乘法累加或 `1` 个 `MACC`。\n\n注意相同 `FLOPs` 的两个模型其运行速度是会相差很多的，因为影响模型运行速度的两个重要因素只通过 `FLOPs` 是考虑不到的，比如 `MAC`（`Memory Access Cost`）和网络并行度；二是具有相同 `FLOPs` 的模型在不同的平台上可能运行速度不一样。\n\n> 注意，网上很多文章将 MACCs 与 MACC 概念搞混，我猜测可能是机器翻译英文文章不准确的缘故，可以参考此[链接](http://machinethink.net/blog/how-fast-is-my-model/)了解更多。需要指出的是，现有很多硬件都将**乘加运算作为一个单独的指令**。\n\n**卷积层 FLOPs 计算**：\n\n> 卷积操作本质上是个线性运算，假设卷积核大小相等且为 $K$。这里给出的公式写法是为了方便理解，大多数时候为了方便记忆，会写成比如 $MACCs = H \\times W \\times K^2 \\times C_i \\times C_o$。\n\n+ $FLOPs=(2\\times C_i\\times K^2-1)\\times H\\times W\\times C_o$（不考虑bias）\n+ $FLOPs=(2\\times C_i\\times K^2)\\times H\\times W\\times C_o$（考虑bias）\n+ $MACCs=(C_i\\times K^2)\\times H\\times W\\times C_o$（考虑bias）\n\n**$C_i$ 为输入特征图通道数，$K$ 为过卷积核尺寸，$H,W,C_o$ 为输出特征图的高，宽和通道数**。`二维卷积过程`如下图所示：\n\n> 二维卷积是一个相当简单的操作：从卷积核开始，这是一个小的权值矩阵。这个卷积核在 2 维输入数据上「滑动」，对当前输入的部分元素进行矩阵乘法，然后将结果汇为单个输出像素。\n\n![卷积过程](../images/flops/conv_dynamic_visual.gif)\n> 图片来源 [Multi-Label Classification and Class Activation Map on Fashion-MNIST](https://towardsdatascience.com/multi-label-classification-and-class-activation-map-on-fashion-mnist-1454f09f5925)。\n\n公式解释如下：\n\n**理解 `FLOPs` 的计算公式分两步**。括号内是第一步，**即先计算出`output feature map` 的一个 `pixel`，然后再乘以 $H\\times W\\times C_o$**，从而拓展到整个 output feature map。括号内的部分又可以分为两步：$(2\\times C_i\\times K^2-1)=(C_i\\times K^2) + (C_i\\times K^2-1)$。第一项是乘法运算次数，第二项是加法运算次数，因为 $n$ 个数相加，要加 $n-1$次，所以不考虑 `bias` 的情况下，会有一个 -1，如果考虑 `bias`，刚好中和掉，括号内变为$(2\\times C_i\\times K^2)$。\n\n所以卷积层的 $FLOPs=(2\\times C_{i}\\times K^2-1)\\times H\\times W\\times C_o$ ($C_i$ 为输入特征图通道数，$K$ 为过滤器尺寸，$H, W, C_o$为输出特征图的高，宽和通道数)。\n\n**全连接层的 FLOPs 计算**：\n\n假设 $I$ 是输入层的维度，$O$ 是输出层的维度。\n\n- 不考虑 bias，全连接层的 $FLOPs = (I + I -1) \\times O = (2I − 1)O$\n- 考虑 bias，全连接层的 $FLOPs = (I + I -1) \\times O + O = (2\\times I)\\times O$\n\n### 1.1 计算利用率(Utilization)\n\n在这种情况下，利用率（Utilization）是可以有效地用于实际工作负载的芯片的原始计算能力的百分比。深度学习和神经网络使用相对数量较少的计算原语（computational primitives），而这些数量很少的计算原语却占用了大部分计算时间。矩阵乘法（MM）和转置是基本操作。MM 由乘法累加（MAC）操作组成。OPs/s（每秒完成操作的数量）指标通过每秒可以完成多少个 MAC（每次乘法和累加各被认为是 1 个 operation，因此 MAC 实际上是 2 个 OP）得到。所以我们可以将利用率定义为实际使用的运算能力和原始运算能力的比值：\n\n$$ mac\\ utilization = \\frac {used\\ Ops/s}{raw\\ OPs/s} = \\frac {FLOPs/time(s)}{Raw\\_FLOPs}(Raw\\_FLOPs = 1.7T\\ at\\ 3519)$$\n\n## 二 模型参数量计算\n\n模型参数数量（params）：指模型含有多少参数，直接决定模型的大小，也影响推断时对内存的占用量，单位通常为`M`，`GPU`端通常参数用`float32`表示，所以模型大小是参数数量的 `4` 倍。这里考虑的卷积核长宽是相同的一般情况，都为 `K`。\n\n**卷积层权重参数量** =  $ C_i\\times K^2\\times C_o + C_o$。\n\n$C_i$ 为输入特征图通道数，$K$ 为过滤器(卷积核)尺寸，$C_o$ 为输出的特征图的 `channel` 数(也是 `filter` 的数量)，算式第二项是偏置项的参数量 。(一般不写偏置项，偏置项对总参数量的数量级的影响可以忽略不记，这里为了准确起见，把偏置项的参数量也考虑进来。）\n\n假设输入层矩阵维度是 96×96×3，第一层卷积层使用尺寸为 5×5、深度为 16 的过滤器（卷积核尺寸为 5×5、卷积核数量为 16），那么这层卷积层的参数个数为 ５×5×3×16+16=1216个。\n\n**`BN` 层参数量** =  $2\\times C_i$。\n\n其中 $C_i$ 为输入的 `channel` 数（BN层有两个需要学习的参数，平移因子和缩放因子）\n\n**全连接层参数量** =  $T_i\\times T_o + T_O$。\n\n$T_i$ 为输入向量的长度， $T_o$ 为输出向量的长度，公式的第二项为偏置项参数量。(目前全连接层已经逐渐被 `Global Average Pooling` 层取代了。) 注意，全连接层的权重参数量（内存占用）远远大于卷积层。\n\n### 2.1 内存访问代价计算\n\n`MAC`(`memory access cost`)  内存访问代价也叫内存使用量，指的是输入单个样本（一张图像），模型/卷积层完成一次前向传播所发生的内存交换总量，即模型的空间复杂度，单位是 `Byte`。\n\n模型参数量的分析是为了了解内存占用情况，内存带宽在某些情况下比 `FLOPs` 更重要，毕竟在目前的计算机结构下，单次内存访问比单次运算慢得多的多。`CNN` 网络中每个网络层 `MAC` 的计算分为：\n- 读输入 `feature map` 大小（`DDR` 读）、\n- 权重大小（`DDR` 读）和\n- 写输出 `feature map` 大小（`DDR` 写）三部分。\n\n以卷积层为例计算 `MAC`，可假设某个卷积层输入 `feature map` 大小是 (`Cin, Hin, Win`)，输出 `feature map` 大小是 (`Hout, Wout, Cout`)，卷积核是 (`Cout, Cin, K, K`)，理论 MAC（理论 MAC 一般小于 实际 MAC）计算公式如下：\n\n```python\n# 端侧推理IN8量化后模型，单位一般为 1 byte\ninput = Hin x Win x Cin  # 输入 feature map 大小\noutput = Hout x Wout x Cout  # 输出 feature map 大小\nweights = K x K x Cin x Cout + bias   # bias 是卷积层偏置\nddr_read = input +  weights\nddr_write = output\nMAC = ddr_read + ddr_write\n```\n> `feature map` 大小一般表示为 （`N, C, H, W`），`MAC` 指标一般用在端侧模型推理中，端侧模型推理模式一般都是单帧图像进行推理，即 `N = 1(batch_size = 1)`，不同于模型训练时的 `batch_size` 大小一般大于 1。\n\n## 三 模型计算量和参数量单位\n\n### 3.1 浮点计算能力\n\n`FLOPS`：每秒浮点运算次数，每秒所执行的浮点运算次数，浮点运算包括了所有涉及小数的运算，比整数运算更费时间。下面几个是表示浮点运算能力的单位。我们一般常用 `TFLOPS(Tops)` 作为衡量 `NPU/GPU` 性能/算力的指标，比如海思 `3519AV100` 芯片的算力为 `1.7Tops` 神经网络运算性能。\n\n+ `MFLOPS`（megaFLOPS）：等于每秒一佰万（=10^6）次的浮点运算。\n+ `GFLOPS`（gigaFLOPS）：等于每秒十亿（=10^9）次的浮点运算。\n+ `TFLOPS`（teraFLOPS）：等于每秒万亿（=10^12）次的浮点运算。\n+ `PFLOPS`（petaFLOPS）：等于每秒千万亿（=10^15）次的浮点运算。\n+ `EFLOPS`（exaFLOPS）：等于每秒百亿亿（=10^18）次的浮点运算。\n\n `params` : 模型参数量，模型的大小由模型参数量决定。params 通常以单位“百万”（million）或“十亿”（billion）表示，具体地：\n\n- `M` 表示百万，是 million 的缩写。因此，当我们说一个模型的参数量为 100M 时，就表示模型有 1 亿个参数，即 100,000,000 个参数。\n- `B` 表示十亿，是 billion 的缩写。当我们说一个模型的参数量为 2B 时，就表示模型有 20 亿个参数，即 2,000,000,000 个参数。\n\n### 3.2 双精度、单精度和半精度\n\n`CPU/GPU` 的浮点计算能力得区分不同精度的浮点数，分为双精度 `FP64`、单精度 `FP32` 和半精度 `FP16`。因为采用不同位数的浮点数的表达精度不一样，所以造成的计算误差也不一样，对于需要处理的数字范围大而且需要精确计算的科学计算来说，就要求采用双精度浮点数，而对于常见的多媒体和图形处理计算，`32` 位的单精度浮点计算已经足够了，对于要求精度更低的机器学习等一些应用来说，半精度 `16` 位浮点数就可以甚至 `8` 位浮点数就已经够用了。\n对于浮点计算来说， `CPU` 可以同时支持不同精度的浮点运算，但在 `GPU` 里针对单精度和双精度就需要各自独立的计算单元。\n\n值得注意的是，模型参数所占用的存储空间取决于参数的**数据类型和精度**，常见的有:\n\n- `FP64`: 双精度浮点数（64位，8 字节）。\n\n- `FP32`: 单精度浮点数（32位，4 字节），包含 1 个符号位、8 个指数位和 23 个⼩数位。\n- `FP16`: 半精度浮点数（16位，2字节），包含 1 个符号位、5 个指数位和 10 个⼩数位。\n- `BF16` : IEEE754 `FP32` 的截断格式（16位，2字节），包含 1 个符号位，8个指数位，7个小数位。\n\n总结：与 FP32 相比，采用 BF16/FP16 吞吐量（Throughput）基本可以翻倍，内存（RAM）需求可以减半。但是这两者精度上差异不一样，BF16 可表示的整数范围更广泛，它和 `float32` 的动态范围是等效的，但是尾数精度较小；FP16 表示整数范围较小，但是尾数精度较高。\n\n![](../images/flops/fp32_fp16_bf16.png)\n\n### 3.3 bfloat16 精度\n\n`bfloat16` 是谷歌开发的另一种 `16` 位格式，全称 “Brain Floating Point Format”。最初的 IEEE FP16 格式并不是针对深度学习应用而设计的，其**动态范围过窄**。BFLOAT16 的提出就是为了解决这个问题，提供了与 FP32 相同的动态范围。\n\nbfloat16 的格式如下所示：\n\n- 符号位：1 bit \n- 指数宽度：8bit\n- 尾数精度：7bit，而不是经典单精度浮点格式中的 24 位\n\n![bfloat16 的格式](../images/flops/bfloat16_format.png)\n> 图片来源 [weiki-bfloat16 floating-point format](https://en.wikipedia.org/wiki/Bfloat16_floating-point_format)。\n\nbfloat16 格式其实就是截断的 IEEE 754 FP32，可以和 IEEE 754 FP32 格式快速转换。在转换为 bfloat16 格式时，**指数位被保留，而有效数字字段直接通过截断来减少**，且忽略 NaN 特殊情况。\n\n神经网络对指数的大小比尾数的大小更灵敏，且与通常需要进行特殊处理（如损失扩缩）的 `float16` 不同，`bfloat16` 是在训练和运行深度神经网络时可以直接替代 `float32`。因此，Google 硬件团队为 Cloud TPU 选择了 `bfloat16`，用于提高硬件效率，同时保持准确训练深度学习模型的能力，并将 `float32` 的转换费用降至最低。\n\n### 3.4 参数量/计算量分析工具\n\n1. [torchinfo](https://github.com/TylerYep/torchinfo): torchsummay （不再更新）的替代版本，可以一键输出 `pytorch` 模型每层输出 `feature map` 大小和参数量、以及模型总的参数量 `params`、计算量 `MACs` 等信息，**支持多输入模式**。\n2. [thop: pytorch-OpCounter](https://github.com/Lyken17/pytorch-OpCounter): 一键输出模型总的计算量 `MACs` 和参数量 `params`，支持输入自定义算子。\n\n两个工具的安装都很简单，如直接 `pip install torchinfo` 即可。统计 `resnet50` 模型的参数量和计算量的示例代码如下所示。\n\n```python\n####################卷积神经网络计算量/参数量分析工具#####################\nimport torchvision, torch\n\nmodel = torchvision.models.resnet50()\n\n# 1, pytorch 自带输出\n# print(model)\n\n# 2, torchinfo 工具\nfrom torchinfo import summary\nsummary(model, (1, 3, 224, 224), depth=3) # resnet50: 25.557M 4.09G\n\n# 3, thop 工具\nfrom thop import profile, clever_format\ninput = torch.randn(1, 3, 224, 224)\nmacs, params = profile(model, inputs=(input, ))\nmacs, params = clever_format([macs, params], \"%.3f\")\nprint(\"The resnet50 model info: \", macs, params) # resnet50: 4.134G 25.557M\n```\n\n模型输出结果如下所示\n\n```bash\n==========================================================================================\nLayer (type:depth-idx)                   Output Shape              Param #\n==========================================================================================\nResNet                                   [1, 1000]                 --\n├─Conv2d: 1-1                            [1, 64, 112, 112]         9,408\n├─BatchNorm2d: 1-2                       [1, 64, 112, 112]         128\n├─ReLU: 1-3                              [1, 64, 112, 112]         --\n├─MaxPool2d: 1-4                         [1, 64, 56, 56]           --\n├─Sequential: 1-5                        [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-1                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-1                  [1, 64, 56, 56]           4,096\n│    │    └─BatchNorm2d: 3-2             [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-3                    [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-4                  [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-5             [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-6                    [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-7                  [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-8             [1, 256, 56, 56]          512\n│    │    └─Sequential: 3-9              [1, 256, 56, 56]          16,896\n│    │    └─ReLU: 3-10                   [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-2                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-11                 [1, 64, 56, 56]           16,384\n│    │    └─BatchNorm2d: 3-12            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-13                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-14                 [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-15            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-16                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-17                 [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-18            [1, 256, 56, 56]          512\n│    │    └─ReLU: 3-19                   [1, 256, 56, 56]          --\n│    └─Bottleneck: 2-3                   [1, 256, 56, 56]          --\n│    │    └─Conv2d: 3-20                 [1, 64, 56, 56]           16,384\n│    │    └─BatchNorm2d: 3-21            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-22                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-23                 [1, 64, 56, 56]           36,864\n│    │    └─BatchNorm2d: 3-24            [1, 64, 56, 56]           128\n│    │    └─ReLU: 3-25                   [1, 64, 56, 56]           --\n│    │    └─Conv2d: 3-26                 [1, 256, 56, 56]          16,384\n│    │    └─BatchNorm2d: 3-27            [1, 256, 56, 56]          512\n│    │    └─ReLU: 3-28                   [1, 256, 56, 56]          --\n├─Sequential: 1-6                        [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-4                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-29                 [1, 128, 56, 56]          32,768\n│    │    └─BatchNorm2d: 3-30            [1, 128, 56, 56]          256\n│    │    └─ReLU: 3-31                   [1, 128, 56, 56]          --\n│    │    └─Conv2d: 3-32                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-33            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-34                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-35                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-36            [1, 512, 28, 28]          1,024\n│    │    └─Sequential: 3-37             [1, 512, 28, 28]          132,096\n│    │    └─ReLU: 3-38                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-5                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-39                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-40            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-41                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-42                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-43            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-44                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-45                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-46            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-47                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-6                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-48                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-49            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-50                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-51                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-52            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-53                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-54                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-55            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-56                   [1, 512, 28, 28]          --\n│    └─Bottleneck: 2-7                   [1, 512, 28, 28]          --\n│    │    └─Conv2d: 3-57                 [1, 128, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-58            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-59                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-60                 [1, 128, 28, 28]          147,456\n│    │    └─BatchNorm2d: 3-61            [1, 128, 28, 28]          256\n│    │    └─ReLU: 3-62                   [1, 128, 28, 28]          --\n│    │    └─Conv2d: 3-63                 [1, 512, 28, 28]          65,536\n│    │    └─BatchNorm2d: 3-64            [1, 512, 28, 28]          1,024\n│    │    └─ReLU: 3-65                   [1, 512, 28, 28]          --\n├─Sequential: 1-7                        [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-8                   [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-66                 [1, 256, 28, 28]          131,072\n│    │    └─BatchNorm2d: 3-67            [1, 256, 28, 28]          512\n│    │    └─ReLU: 3-68                   [1, 256, 28, 28]          --\n│    │    └─Conv2d: 3-69                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-70            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-71                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-72                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-73            [1, 1024, 14, 14]         2,048\n│    │    └─Sequential: 3-74             [1, 1024, 14, 14]         526,336\n│    │    └─ReLU: 3-75                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-9                   [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-76                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-77            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-78                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-79                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-80            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-81                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-82                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-83            [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-84                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-10                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-85                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-86            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-87                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-88                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-89            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-90                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-91                 [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-92            [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-93                   [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-11                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-94                 [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-95            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-96                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-97                 [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-98            [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-99                   [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-100                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-101           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-102                  [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-12                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-103                [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-104           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-105                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-106                [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-107           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-108                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-109                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-110           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-111                  [1, 1024, 14, 14]         --\n│    └─Bottleneck: 2-13                  [1, 1024, 14, 14]         --\n│    │    └─Conv2d: 3-112                [1, 256, 14, 14]          262,144\n│    │    └─BatchNorm2d: 3-113           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-114                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-115                [1, 256, 14, 14]          589,824\n│    │    └─BatchNorm2d: 3-116           [1, 256, 14, 14]          512\n│    │    └─ReLU: 3-117                  [1, 256, 14, 14]          --\n│    │    └─Conv2d: 3-118                [1, 1024, 14, 14]         262,144\n│    │    └─BatchNorm2d: 3-119           [1, 1024, 14, 14]         2,048\n│    │    └─ReLU: 3-120                  [1, 1024, 14, 14]         --\n├─Sequential: 1-8                        [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-14                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-121                [1, 512, 14, 14]          524,288\n│    │    └─BatchNorm2d: 3-122           [1, 512, 14, 14]          1,024\n│    │    └─ReLU: 3-123                  [1, 512, 14, 14]          --\n│    │    └─Conv2d: 3-124                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-125           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-126                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-127                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-128           [1, 2048, 7, 7]           4,096\n│    │    └─Sequential: 3-129            [1, 2048, 7, 7]           2,101,248\n│    │    └─ReLU: 3-130                  [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-15                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-131                [1, 512, 7, 7]            1,048,576\n│    │    └─BatchNorm2d: 3-132           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-133                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-134                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-135           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-136                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-137                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-138           [1, 2048, 7, 7]           4,096\n│    │    └─ReLU: 3-139                  [1, 2048, 7, 7]           --\n│    └─Bottleneck: 2-16                  [1, 2048, 7, 7]           --\n│    │    └─Conv2d: 3-140                [1, 512, 7, 7]            1,048,576\n│    │    └─BatchNorm2d: 3-141           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-142                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-143                [1, 512, 7, 7]            2,359,296\n│    │    └─BatchNorm2d: 3-144           [1, 512, 7, 7]            1,024\n│    │    └─ReLU: 3-145                  [1, 512, 7, 7]            --\n│    │    └─Conv2d: 3-146                [1, 2048, 7, 7]           1,048,576\n│    │    └─BatchNorm2d: 3-147           [1, 2048, 7, 7]           4,096\n│    │    └─ReLU: 3-148                  [1, 2048, 7, 7]           --\n├─AdaptiveAvgPool2d: 1-9                 [1, 2048, 1, 1]           --\n├─Linear: 1-10                           [1, 1000]                 2,049,000\n==========================================================================================\nTotal params: 25,557,032\nTrainable params: 25,557,032\nNon-trainable params: 0\nTotal mult-adds (G): 4.09\n==========================================================================================\nInput size (MB): 0.60\nForward/backward pass size (MB): 177.83\nParams size (MB): 102.23\nEstimated Total Size (MB): 280.66\n==========================================================================================\n[INFO] Register count_convNd() for <class 'torch.nn.modules.conv.Conv2d'>.\n[INFO] Register count_normalization() for <class 'torch.nn.modules.batchnorm.BatchNorm2d'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.activation.ReLU'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.pooling.MaxPool2d'>.\n[INFO] Register zero_ops() for <class 'torch.nn.modules.container.Sequential'>.\n[INFO] Register count_adap_avgpool() for <class 'torch.nn.modules.pooling.AdaptiveAvgPool2d'>.\n[INFO] Register count_linear() for <class 'torch.nn.modules.linear.Linear'>.\n```\n\n值得注意的是，不同工具统计出来的模型计算量和参数量可能不一样，因为计算方式不一样，但是都是比较准确的。使用 `thop` 工具统计的经典 backbone 的 `Params` 参数量与 `FLOPs` 计算量如下表所示：\n\n![模型复杂度分析结果](../images/flops/thop_summary.png)\n\n## 参考资料\n\n+ [PRUNING CONVOLUTIONAL NEURAL NETWORKS FOR RESOURCE EFFICIENT INFERENCE](https://arxiv.org/pdf/1611.06440.pdf)\n+ [神经网络参数量的计算：以UNet为例](https://zhuanlan.zhihu.com/p/57437131)\n+ [How fast is my model?](http://machinethink.net/blog/how-fast-is-my-model/)\n+ [MobileNetV1 & MobileNetV2 简介](https://blog.csdn.net/mzpmzk/article/details/82976871)\n+ [双精度，单精度和半精度](https://blog.csdn.net/sinat_24143931/article/details/78557852?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase)\n+ [AI硬件的Computational Capacity详解](https://zhuanlan.zhihu.com/p/27836831)\n+ [Roofline Model与深度学习模型的性能分析](https://zhuanlan.zhihu.com/p/34204282)\n+ [目标检测的性能评价指标](https://zhuanlan.zhihu.com/p/70306015?utm_source=wechat_session&utm_medium=social&utm_oi=571954943427219456)\n+ [FP64, FP32, FP16, BFLOAT16, TF32, and other members of the ZOO](https://moocaholic.medium.com/fp64-fp32-fp16-bfloat16-tf32-and-other-members-of-the-zoo-a1ca7897d407)\n"
  },
  {
    "path": "6-model_deploy/模型压缩部署概述.md",
    "content": "\n- [计算机相关性能指标](#计算机相关性能指标)\n- [一，模型在线部署](#一模型在线部署)\n  - [1.1，深度学习项目开发流程](#11深度学习项目开发流程)\n  - [1.2，模型训练和推理的不同](#12模型训练和推理的不同)\n- [二，手机端CPU推理框架的优化](#二手机端cpu推理框架的优化)\n- [三，不同硬件平台量化方式总结](#三不同硬件平台量化方式总结)\n- [参考资料](#参考资料)\n\n## 计算机相关性能指标\n\n- **延迟**是一个操作从开始到完成所需要的时间，常用微秒来表示。\n- **带宽**是单位时间内可处理的数据量，通常表示为 MB/s或GB/s。\n- **吞吐量**是单位时间内成功处理的运算数量，通常表示为gflops（即每秒十亿次的浮点运算数量），特别是在重点使用浮点计算的科学计算领域经常用\n到。\n\n总结：**延迟用来衡量完成一次操作的时间，而吞吐量用来衡量在给定的单位时间内处理的操作量**。\n\n## 一，模型在线部署\n\n深度学习和计算机视觉方向除了算法训练/研究，还有两个重要的方向: 模型压缩（模型优化、量化）、模型部署（模型转换、后端功能 SDK开发等）。所谓模型部署，即将算法研究员训练出的模型部署到具体的**端边云**芯片平台上，并完成特定业务的应用开发，比如：\n- 封装成一个算法 SDK，集成到 APP 或者服务中；\n- 封装成一个 web 服务，对外暴露接口（HTTP(S)，RPC 等协议）\n\n现阶段的部署平台主要分为**云平台**（如英伟达 `GPU`）、手机**移动端**平台（`ARM` 系列芯片）和其他**嵌入式端侧**平台（海思 `3519`、安霸 `CV22`、地平线 `X3`、英伟达 `jetson tx2` 等芯片）。对于模型部署/移植/优化工程师来说，虽然模型优化、量化等是更有挑战性和技术性的知识，但是对于新手的我们往往是在做解决模型无法在端侧部署的问题，包括但不限于：实现新 `OP`、修改不兼容的属性、修改不兼容的权重形状、学习不同芯片平台的推理部署框架等。对于模型转换来说，现在行业主流是使用 `Caffe` 和 `ONNX` 模型作为中间模型。\n\n### 1.1，深度学习项目开发流程\n\n在高校做深度学习 `demo` 应用一般是这样一个过程，比如使用 `Pytorch/TensorFlow` 框架训练出一个模型，然后直接使用 `Pytorch` 框架做推理（`test`）完成功能验证，但是在工业界这是不可能的，因为这样模型推理速度很慢，一般我们必须有专门的深度学习推理加速框架去做模型推理（`inference`）。以 GPU 云平台推理框架 `TensorRT` 为例，简单描述模型训练推理过程就是：训练好网络模型（权重参数数据类型为 `FP32`）输入 `TensorRT`，然后 `TensorRT` 做解析优化，并进行在线推理和输出结果。两种不同的模型训练推理过程对比如下图所示:\n\n![训练和推理.jpg](../images/deploy_summary/training_and_inference.jpg)\n\n前面的描述较为简单，实际在工业届，理想的深度学习项目开发流程应该分为三个步骤: **模型离线训练、模型压缩和模型在线部署**，后面两个步骤互有交叉，具体详情如下：\n\n1. **模型离线训练**：实时性低，数据离线且更新不频繁，`batchsize` 较大，消耗大量 GPU 资源。\n    + 设计开发模型网络结构;\n    + 准备数据集并进行数据预处理、`EDA` 等操作；\n    + 深度学习框架训练模型：数据增强、超参数调整、优化器选择、训练策略调整（多尺度训练）、`TTA`、模型融合等；\n    + 模型测试。\n\n2. **模型优化压缩**：主要涉及模型优化、模型转换、模型量化和模型编译优化，这些过程很多都在高性能计算推理框架中集成了，各个芯片厂商也提供了相应的工具链和推理库来完成模型优化压缩。实际开发中，在不同的平台选择不同的推理加速引擎框架，比如 `GPU` 平台选择 `TensorRT`，手机移动端（`ARM`）选择 `NCNN/MNN`，`NPU` 芯片平台，如海思3519、地平线X3、安霸CV22等则直接在厂商给出的工具链进行模型的优化（`optimizer`）和压缩。\n    + **模型优化** `Optimizer`：主要指计算图优化。首先对计算图进行分析并应用一系列**与硬件无关的优化策略**，从而在逻辑上降低运行时的开销，常见的类似优化策略其包括：算子融合（`conv、bn、relu` 融合）、算子替换、常数折叠、公共子表达式消除等。\n    + **模型转换** `Converter`：`Pytorch->Caffe`、`Pytorch->ONNX`、`ONNX`模型->`NCNN/NPU芯片厂商模型格式`（需要踩坑非常多，`Pytorch`、`ONNX`、`NPU` 三者之间的算子要注意兼容）。注意 `ONNX` 一般用作训练框架和推理框架之间转换的中间模型格式。\n    + **模型量化** `Quantizer`：主要指训练后量化（Post-training quantization `PTQ`）；权重、激活使用不同的量化位宽，如速度最快的量化方式 `w8a8`、速度和精度平衡的量化方式 `w8a16`。\n    + **模型编译优化**（编译优化+`NPU` 指令生成+内存优化）`Compiler`：**模型编译针对不同的硬件平台有不同优化方法**，与前面的和硬件无关的模型层面的优化不同。`GPU`平台存在 `kernel fusion` 方法；而 `NPU` 平台算子是通过特定二进制指令实现，其编译优化方法包括，卷积层的拆分、卷积核权重数据重排、`NPU` 算子调优等。\n\n3. **模型部署/SDK输出**: 针对视频级应用需要输出功能接口的SDK。实时性要求高，数据线上且更新频繁，`batchsize` 为 1。主要需要完成多模型的集成、模型输入的预处理、非DL算法模块的开发、 各个模块 `pipeline` 的串联，以及最后 `c` 接口（`SDK`）的输出。\n    + **板端框架模型推理**: `Inference`：`C/C++`。不同的 `NPU` 芯片/不同的公司有着不同的推理框架，但是模型的推理流程大致是一样的。包括：输入图像数据预处理、加载模型文件并解析、填充输入图像和模型权重数据到相应地址、模型推理、释放模型资源。这里主要需要学习不同的模型部署和推理框架。\n    + **pipeline 应用开发**: 在实际的深度学习项目开发过程中，模型推理只是其中的基础功能，具体的我们还需要实现多模型的集成、模型输入前处理、以及非 `DL` 算法模块的开发: 包括检测模块、跟踪模块、选帧模块、关联模块和业务算法模块等，并将各模块串联成一个 `pipeline`，从而完成视频结构化应用的开发。\n    + **SDK集成**: 在完成了具体业务 `pipeline` 的算法开发后，一般就需要输出 `c` 接口的 `SDK` 给到下层的业务侧（前后端）人员调用了。这里主要涉及 `c/c++` 接口的转换、`pipeline` 多线程/多通道等sample的开发、以及大量的单元、性能、精度、稳定性测试。\n    + **芯片平台板端推理** `Inference`，不同的 `NPU` 芯片有着不同的 `SDK` 库代码，但是模型运行流程类似。\n\n> 不同平台的模型的编译优化是不同的，比如 `NPU` 和一般 `GPU` 的区别在于后端模型编译上，`GPU` 是编译生成 `kernel library`(`cuDNN` 函数)，`NPU` 是编译生成二进制指令；前端的计算图优化没有本质区别，基本通用。\n\n所以综上所述，深度学习项目开发流程可以大致总结为三个步骤: **模型离线训练**、**模型优化压缩**和**模型部署/SDK输出**，后两个步骤互有交叉。前面 `2` 个步骤在 `PC` 上完成，最后一个步骤开发的代码是需要在在 `AI` 芯片系统上运行的。最后以视差模型在海思 `3519` 平台的部署为例，其模型部署工作流程如下：\n\n![视差模型在海思3519平台部署工作流程.png](../images/deploy_summary/parallax_model_deployment_process_on_hisilicon_3519_platform.jpg)\n\n### 1.2，模型训练和推理的不同\n\n为了更好进行模型优化和部署的工作，需要总结一下模型推理（`Inference`）和训练（`Training`）的不同：\n\n1. 网络权重值固定，只有前向传播（`Forward`），无需反向传播，因此：\n    + 模型权值和结构固定，可以做计算图优化，比如算子融合等；\n    + 输入输出大小固定，可以做 `memory` 优化，比如 `feature` 重排和 `kernel` 重排。\n2. `batch_size` 会很小（比如 `1`），存在 `latency` 的问题。\n3. 可以使用低精度的技术，训练阶段要进行反向传播，每次梯度的更新是很微小的，需要相对较高的精度比如 `FP32` 来处理数据。但是推理阶段，对精度要求没那么高，现在很多论文都表明使用低精度如 `in16` 或者 `int8` 数据类型来做推理，也不会带来很大的精度损失。\n\n## 二，手机端CPU推理框架的优化\n\n对于 `HPC` 和软件工程师来说，在手机 `CPU` 端做模型推理框架的优化，可以从上到下考虑：\n\n1. **算法层优化**：最上面就是算法层，如可以用winograd从数学上减少乘法的数量（仅在大channel尺寸下有效）；\n2. **框架优化**：推理框架可以实现内存池、多线程等策略；\n3. **硬件层优化**：主要包括: 适应不同的硬件架构特性、`pipeline`和`cache`优化、内存数据重排、`NEON` 汇编优化等。\n\n## 三，不同硬件平台量化方式总结\n\n| 芯片厂商 | 芯片型号 | 支持方式 | 支持精度 | 量化方式/范围 |量化工具|\n| ------ | -------- | ------ | ------ | ----------- |--------|\n| 华为     | Hisi系列3519A/3559A/3516C等|  整网编译|int16/int8 |非线性(对数) 量化 |nnie_mapper|\n|Ambarella|CV22/CV25|整网编译|int8/int16|支持权重激活选择不同的位宽量化、自动搜索最优的量化策略|工具链CNNGen 的 Parsers|\n|Nvidia|全系列GPU|整网编译/CUDA C|fp32/fp16/int8/int4/int1|`TensorRT`: 非对称 KL 散度 + `per-channel/per-layer` 量化|`TensorRT` 框架|\n|Qualcomm|全系列 SoC\t|整网编译|fp32/fp16/int8|**非对称最大最小值量化** + `per-layer` 量化|AIMET 模型量化压缩工具|\n|Rockchips|RV1108/RV1109/RV1126等|整网编译|`int16/int8`|非对称量化/混合量化|RKNN Toolkit2|\n\nNVIDIA 的 `TensorRT` 框架在对权值(weights) 的量化上支持 `per-tensor`(也叫 per-layer) 和 `per-channel` 两种方式，采用**对称最大值**的方法；对于激活值(activations) 只支持 per-tensor 的方式，采用 `KL-divergence` 的方法进行量化。\n\n## 参考资料\n\n1. 《NVIDIA TensorRT 以及实战记录》PPT\n"
  },
  {
    "path": "6-model_deploy/模型推理加速技巧-融合卷积和BN层.md",
    "content": "## 前言\n\n**模型推理的两个主要挑战是延迟和成本**。\n\n## 参考资料\n\n1. [Speeding up model with fusing batch normalization and convolution](https://learnml.today/speeding-up-model-with-fusing-batch-normalization-and-convolution-3)\n2. [深度学习推理时融合BN，轻松获得约5%的提速](https://mp.weixin.qq.com/s/PXsyy-WBqxA5FTMFseC9Rw)\n3. [CS231n课程笔记翻译：卷积神经网络笔记](https://zhuanlan.zhihu.com/p/22038289?refer=intelligentunit)\n4. [模型推理加速技巧：融合BN和Conv层](https://zhuanlan.zhihu.com/p/110552861)"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2021 OpenPPL\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "- [项目概述](#项目概述)\n- [我的自制大模型推理框架课程介绍](#我的自制大模型推理框架课程介绍)\n- [一，数学和编程基础专栏](#一数学和编程基础专栏)\n- [二，神经网络基础部件](#二神经网络基础部件)\n- [三，经典卷积神经网络模型](#三经典卷积神经网络模型)\n- [四，深度学习炼丹](#四深度学习炼丹)\n- [五，深度学习模型压缩](#五深度学习模型压缩)\n- [六，模型推理部署](#六模型推理部署)\n- [七，进阶课程](#七进阶课程)\n- [参考资料](#参考资料)\n\n## 项目概述\n\n本仓库项目是个人总结的计算机视觉和大语言模型学习笔记，包含深度学习基础知识、神经网络基础部件详解、深度学习炼丹策略、深度学习模型压缩算法、深度学习推理框架代码解析及动手实战。\n\n`LLM` 基础及推理优化的专栏笔记请参考 [llm_note](https://github.com/HarleysZhang/llm_note) 仓库。\n\n## 我的自制大模型推理框架课程介绍\n\n1. **框架亮点**：基于 Triton + PyTorch 开发的轻量级、且简单易用的大模型推理框架，采用类 Pytorch 语法的 Triton 编写算子，绕开 Cuda 复杂语法实现 GPU 加速。\n2. **价格：499**。非常实惠和便宜，课程、项目、面经、答疑质量绝对对得起这个价格。\n3. **课程优势​**：\n   - **手把手教你从 0 到 1 实现大模型推理框架**。\n   - 项目导向 + 面试导向 + **分类总结的面试题**。\n   - 2025 最新的高性能计算/推理框架岗位的大厂面试题汇总\n4. **项目优势​**：\n\t- 架构清晰，代码简洁且注释详尽，覆盖大模型离线推理全流程。​\n    - 运用 OpenAI Triton 编写高性能计算 Kernel，开发矩阵乘法内核，效率堪比 cuBLAS。​\n    - 依托 PyTorch 进行高效显存管理。​\n    - 课程项目完美支持 FlashAttentionV1、V2、V3 与 `GQA`，以及 `PageAttention` 的具体实现。​\n    - 使用 `Triton` 编写融合算子，如 KV 线性层融合等。​\n    - 适配最新的 `llama/qwen2.5/llava1.5` 模型，相较 transformers 库，在 llama3 1B 和 3B 模型上，加速比最高可达 `4` 倍。\n5. **分类总结部分面试题**：\n\n<table style=\"width: 100%; table-layout: fixed;\">\n  <tr>\n    <td align=\"center\"><img src=\"./images/read_me/interview1.png\" width=\"100%\" alt=\"llava_output2\"></td>\n    <td align=\"center\"><img src=\"./images/read_me/interview2.png\" width=\"100%\" alt=\"llava_output\"></td>\n  </tr>\n</table>\n\n6. **项目运行效果**:\n\n`llama3.2-1.5B-Instruct` 模型流式输出结果测试：\n\n![流式输出](./images/read_me/generate.gif)\n\n`Qwen2.5-3B` 模型（社区版本）流式输出结果测试：\n\n![流式输出](./images/read_me/output.gif)\n\n`Llava1.5-7b-hf` 模型流式输出结果测试:\n\n<table style=\"width: 100%; table-layout: fixed;\">\n  <tr>\n    <td align=\"center\"><img src=\"./images/read_me/llava_output2.gif\" width=\"90%\" alt=\"llava_output2\"></td>\n    <td align=\"center\"><img src=\"./images/read_me/llava_output1.gif\" width=\"100%\" alt=\"llava_output\"></td>\n  </tr>\n</table>\n\n感兴趣的同学可以扫码联系课程购买，这个课程是我和[《自制深度学习推理框架》作者](https://space.bilibili.com/1822828582)一起合力打造的，内容也会持续更新优化。\n\n<div align=\"center\">\n<img src=\"./images/read_me/fu_qcode.jpg\" width=\"40%\" alt=\"transformer_block_mp\">\n</div>\n\n## 一，数学和编程基础专栏\n\n- [深度学习数学基础-概率与信息论](./1-math_ml_basic/深度学习数学基础-概率与信息论.md)\n- [深度学习基础-机器学习基本原理](./1-math_ml_basic/深度学习基础-机器学习基本原理.md)\n- [随机梯度下降法的数学基础](./1-math_ml_basic/随机梯度下降法的数学基础.md)\n- [Python 编程思维导航](./1-math_ml_basic/python_learn_xmind)\n\n## 二，神经网络基础部件\n\n1，**神经网络基础部件**：\n\n1. [神经网络基础部件-卷积层详解](./2-deep_learning_basic/神经网络基础部件-卷积层详解.md)\n2. [神经网络基础部件-BN 层详解](./2-deep_learning_basic/神经网络基础部件-BN层详解.md)\n3. [神经网络基础部件-激活函数详解](./2-deep_learning_basic/神经网络基础部件-激活函数详解.md)\n\n2，**深度学习基础**：\n\n- [反向传播与梯度下降详解](2-deep_learning_basic/反向传播与梯度下降详解.md)\n- [深度学习基础-参数初始化详解](./2-deep_learning_basic/深度学习基础-参数初始化详解.md)\n- [深度学习基础-损失函数详解](./2-deep_learning_basic/深度学习基础-损失函数详解.md)\n- [深度学习基础-优化算法详解](./2-deep_learning_basic/深度学习基础-优化算法详解.md)\n\n## 三，经典卷积神经网络模型\n\n**1，卷积神经网络的经典 backbone**：\n\n- [ResNet网络详解](3-classic_backbone/ResNet网络详解.md)\n- [DenseNet 网络详解](3-classic_backbone/DenseNet论文解读.md)\n- [ResNetv2 网络详解](3-classic_backbone/ResNetv2论文解读.md)\n- [经典 backbone 网络总结](3-classic_backbone/经典backbone总结.md)\n\n**2，轻量级网络详解**：\n\n- [MobileNetv1论文详解](3-classic_backbone/efficient_cnn/MobileNetv1论文详解.md)\n- [ShuffleNetv2论文详解](3-classic_backbone/efficient_cnn/ShuffleNetv2论文详解.md)\n- [RepVGG论文详解](3-classic_backbone/efficient_cnn/RepVGG论文详解.md)\n- [CSPNet论文详解](3-classic_backbone/efficient_cnn/CSPNet论文详解.md)\n- [VoVNet论文解读](3-classic_backbone/efficient_cnn/VoVNet论文解读.md)\n- [轻量级模型设计总结](5-model_compression/模型压缩-轻量化网络总结.md)\n\n## 四，深度学习炼丹\n\n1. [深度学习炼丹-数据标准化](./4-deep_learning_alchemy/深度学习炼丹-数据标准化.md)\n2. [深度学习炼丹-数据增强](./4-deep_learning_alchemy/深度学习炼丹-数据增强.md)\n3. [深度学习炼丹-不平衡样本的处理](./4-deep_learning_alchemy/深度学习炼丹-不平衡样本的处理.md)\n4. [深度学习炼丹-超参数设定](./4-deep_learning_alchemy/深度学习炼丹-超参数调整.md)\n5. [深度学习炼丹-正则化策略](./4-deep_learning_alchemy/深度学习炼丹-正则化策略.md)\n\n## 五，深度学习模型压缩\n\n1. [深度学习模型压缩算法综述](./5-model_compression/深度学习模型压缩方法概述.md)\n2. [模型压缩-轻量化网络设计与部署总结](./5-model_compression/模型压缩-轻量化网络详解.md)\n3. [模型压缩-剪枝算法详解](./5-model_compression/模型压缩-剪枝算法详解.md)\n4. [模型压缩-知识蒸馏详解](./5-model_compression/模型压缩-知识蒸馏详解.md)\n5. [模型压缩-量化算法详解](./5-model_compression/模型压缩-量化算法概述.md)\n\n## 六，模型推理部署\n\n1，模型推理部署：\n\n- [卷积神经网络复杂度分析](./6-model_deploy/卷积神经网络复杂度分析.md)\n- [模型压缩部署概述](./6-model_deploy/模型压缩部署概述.md)\n- [矩阵乘法详解](./6-model_deploy/卷积算法优化.md)\n- [模型推理加速技巧-融合卷积和BN层](./6-model_deploy/模型推理加速技巧-融合卷积和BN层.md)\n\n2，`ncnn` 框架源码解析：\n\n- [ncnn 源码解析-sample 运行](5-model_deploy/ncnn源码解析-sample运行.md)\n- [ncnn 源码解析-Net 类](5-model_deploy/ncnn源码解析-Net类.md)\n\n3，异构计算\n\n1. 移动端异构计算：`neon` 编程\n2. GPU 端异构计算：`cuda` 编程，比如 `gemm` 算法解析与优化\n\n## 七，进阶课程\n\n1，推荐几个比较好的深度学习模型压缩与加速的仓库和课程资料：\n\n1. [神经网络基本原理教程](https://github.com/microsoft/ai-edu/blob/master/%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.1-%E5%8D%B7%E7%A7%AF%E7%9A%84%E5%89%8D%E5%90%91%E8%AE%A1%E7%AE%97%E5%8E%9F%E7%90%86.md)\n2. [AI-System](https://microsoft.github.io/AI-System/): 深度学习系统，主要从底层方向讲解深度学习系统等原理、加速方法、矩阵成乘加计算等。\n3. [pytorch-deep-learning](https://github.com/mrdbourke/pytorch-deep-learning)：很好的 pytorch 深度学习教程。\n\n2，一些笔记好的博客链接：\n\n- [The Illustrated Transformer](http://jalammar.github.io/illustrated-transformer/): 国内比较好的博客大都参考这篇文章。\n- [C++ 并发编程（从C++11到C++17）](https://paul.pub/cpp-concurrency/): 不错的 C++ 并发编程教程。\n- [What are Diffusion Models?](https://lilianweng.github.io/posts/2021-07-11-diffusion-models/)\n- [annotated_deep_learning_paper_implementations](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n\n3，最后，持续高质量创作不易，有 `5` 秒空闲时间的，**可以扫码关注我的公众号-嵌入式视觉**，记录 CV 算法工程师成长之路，分享技术总结、读书笔记和个人感悟。\n> 公众号不会写标题党文章，也不输出给大家带来的焦虑的内容！\n\n![qcode](images/others/qcode.png)\n\n4，Star History Chart：\n\n[![Star History Chart](https://api.star-history.com/svg?repos=HarleysZhang/deep_learning_system&type=Date)](https://star-history.com/#HarleysZhang/deep_learning_system&Date)\n\n## 参考资料\n\n- 《深度学习》\n- 《机器学习》\n- 《动手学深度学习》\n- [《机器学习系统：设计和实现》](https://openmlsys.github.io/index.html)\n- [《AI-EDU》](https://ai-edu.openai.wiki/%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/index.html)\n- [《AI-System》](https://github.com/microsoft/AI-System/tree/main/Textbook)\n- [《PyTorch_tutorial_0.0.5_余霆嵩》](https://github.com/TingsongYu/PyTorch_Tutorial)\n- [《动手编写深度学习推理框架 Planer》](https://github.com/Image-Py/planer)\n- [distill：知识精要和在线可视化](https://distill.pub/)\n- [LLVM IR入门指南](https://github.com/Evian-Zhang/llvm-ir-tutorial)\n- [nanoPyC](https://github.com/vesuppi/nanoPyC/tree/master)\n- [ClassifyTemplate](https://github.com/Yale1417/ClassifyTemplate)\n- [pytorch-classification](https://github.com/bearpaw/pytorch-classification)\n"
  },
  {
    "path": "images/dl/courgette.log",
    "content": ""
  },
  {
    "path": "process_image.py",
    "content": "import os\nimport re\nimport time\nfrom deep_translator import GoogleTranslator\n\n# 支持的图片文件扩展名\nIMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg']\n\n# 指定图片目录和Markdown目录\nIMAGE_DIRECTORY = './images/'        # 替换为您的图片目录路径\nMARKDOWN_DIRECTORY = './'  # 替换为您的Markdown目录路径\n\n# 检查文件名是否包含中文字符\ndef contains_chinese(text):\n    return any('\\u4e00' <= char <= '\\u9fff' for char in text)\n\n# 使用deep_translator翻译中文文本\ndef translate_text(text):\n    translator = GoogleTranslator(source='zh-CN', target='en')\n    try:\n        translated_text = translator.translate(text)\n        # 去除特殊字符，只保留字母、数字和下划线\n        translated_text = re.sub(r'\\W+', '_', translated_text)\n        return translated_text.lower()\n    except Exception as e:\n        print(f\"翻译出错：{e}\")\n        return text\n\n# 获取新的文件名，避免重复\ndef get_new_filename(original_name, extension, existing_names):\n    base_name = original_name\n    new_name = base_name\n    counter = 1\n    while new_name + extension in existing_names:\n        new_name = f\"{base_name}_{counter}\"\n        counter += 1\n    return new_name + extension\n\n# 主函数\ndef main():\n    # 记录已重命名的文件映射：原始文件路径 -> 新文件路径\n    filename_mapping = {}\n\n    # 第一步：遍历图片目录，找到中文命名的图片文件并重命名\n    for root, dirs, files in os.walk(IMAGE_DIRECTORY):\n        existing_files = set(files)\n        for filename in files:\n            name, extension = os.path.splitext(filename)\n            if extension.lower() in IMAGE_EXTENSIONS and contains_chinese(name):\n                # 翻译文件名\n                translated_name = translate_text(name)\n                # 获取新的文件名，确保不重复\n                new_filename = get_new_filename(translated_name, extension, existing_files)\n                # 重命名文件\n                old_file_path = os.path.join(root, filename)\n                new_file_path = os.path.join(root, new_filename)\n                os.rename(old_file_path, new_file_path)\n                print(f\"重命名：{old_file_path} -> {new_file_path}\")\n                # 更新映射关系（使用相对路径）\n                relative_old_path = os.path.relpath(old_file_path, start=MARKDOWN_DIRECTORY)\n                relative_new_path = os.path.relpath(new_file_path, start=MARKDOWN_DIRECTORY)\n                filename_mapping[relative_old_path] = relative_new_path\n                # 更新现有文件列表\n                existing_files.remove(filename)\n                existing_files.add(new_filename)\n                # 避免过快处理\n                time.sleep(0.5)\n\n    # 第二步：更新Markdown文件中的图片引用\n    md_extensions = ['.md', '.markdown']\n    for root, dirs, files in os.walk(MARKDOWN_DIRECTORY):\n        for filename in files:\n            name, extension = os.path.splitext(filename)\n            if extension.lower() in md_extensions:\n                file_path = os.path.join(root, filename)\n                with open(file_path, 'r', encoding='utf-8') as f:\n                    content = f.read()\n                # 替换图片文件名\n                updated = False\n                for original_path, new_path in filename_mapping.items():\n                    if original_path in content:\n                        content = content.replace(original_path, new_path)\n                        updated = True\n                if updated:\n                    # 将更新后的内容写回文件\n                    with open(file_path, 'w', encoding='utf-8') as f:\n                        f.write(content)\n                    print(f\"已更新Markdown文件：{file_path}\")\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "互联网技术大佬独立博客推荐.md",
    "content": "1，[bang's blog](http://blog.cnbang.net/about/)\n\n17 年就是蚂蚁金服 P8 的前端大佬。博客质量基本都很高，看他的文章会让我得到些思考，比如文章中的 “心流”（我的叫法跟他的不一样，但内核一样）的描述，我之前也有过类似的状态，但只有一次，就是高三临场抱佛脚，花了 3 周让自己的排名进步了近 1000 名的复习经历，可惜这种状态太难得了，他首先需要的就是无与伦比的专注！\n\n![bang's blog about me](./images/blog/bang's_blog_aboutme.png)\n\n2，[Guibo Wang blog](https://borgwang.github.io/archive)\n\n腾讯广告算法大佬的博客，我是通过他的《手撸一个简单深度学习框架》文章找到他的博客站的，网站文章数量不多，但是基本是高质量！作者部分博客文章截图如下:\n\n![guibo_blog](./images/blog/guibo_blog_chapter.png)\n\n3，[yongyuan blog](https://yongyuan.name/)\n\n快手算法大佬，专注 CBIR（基于内容的图像检索）领域，我是通过他的文章-[图像检索：基于内容的图像检索技术](https://yongyuan.name/blog/cbir-technique-summary.html)和年终总结系列文章关注到他的独立博客网站的，文章内容质量都很高，且叙述逻辑清晰，表达方式易懂。\n\n![年终总结](./images/blog/yuan_yong_summary.png)\n\n4，[hahack](https://www.hahack.com/)\n\n腾讯开发大佬，现在专注教育领域内的产品和课程开发，最早是通过他的叮当开源机器人项目关注到他博客的，后面发现他的技术文章写的都不错，通俗易懂的同时还能兼顾内容深度，尤其是他的年终总结系列看完真的会引起读者的反思。可惜两年多没有更新 `blog` 了，毕竟写 `blog` 才是真为爱发电，现在还在坚守个人 `blog` 站点的多少肯定是有技术信仰和情怀的。\n\n![hahack](./images/blog/hahack.png)\n\n5，[Piglei blog](https://www.piglei.com/)\n\nPython 大佬，他的 《Python 工匠》专栏是我目前看过写的最好的 Python 专栏，文章脉络清晰，原理描述与实例结合得也很好，目前网站还在坚持更新。另外，他的个人 blog 站点是用 Python 框架 Django 开发的。\n\n![Piglei blog](./images/blog/piglei_blog.png)\n\n6，[Victor Zhou blog](https://victorzhou.com/)\n\n国外的一个作者，博客主要是关于 web 开发和机器学习的内容，他的[从零开始入门 CNN 文章堪称经典](https://victorzhou.com/series/neural-networks-from-scratch/)。\n\n![victorzhou_blog](./images/blog/victorzhou_blog.png)\n\n7，[laike9m's blog](https://laike9m.com/blog/wo-de-2019-pycon-china-xiao-jie-xia,127/)\n\nPython 开发大佬，我是通过他的文章-[我的 2019 PyCon China 小结（下）](https://laike9m.com/blog/wo-de-2019-pycon-china-xiao-jie-xia,127/)，关注到他的博客站点的，让我了解到了一些 Python 开发者小组和活动，他的文章还挺有意思的，读起来比较轻松。\n\n![laike9m's_blog](./images/blog/laike9m's_blog.png)\n\n8，[Distill](https://distill.pub/)\n\nDistill 这个网站超级牛逼！文章数目不多，但是，他的文章都是让机器学习研究变得清晰、动态和生动。文章是带交互的，可视化理解神经网络，非常推荐 ml/dl learner 去关注和学习。另外，国外在技术开发工具、框架、知识传播/分享方便比国内还是好很多，像 `Distill` 类似的文章/blog 国外不少，但国内我目前还没看到。\n\n![distill_blog](images/blog/distill_blog.png)\n\n9，[xiaoming blog](https://www.dongwm.com/archives)\n\n豆瓣大佬，之前专注 Python web 开发。我是通过他的文章-[博客十年](https://www.dongwm.com/page/about-blog)关注到博客站点的。在独立博客站点，算是更新很多的，且文章质量普遍都很高。学习 Python 的开发者，如果想深入理解一些 Python 开发知识，还是很推荐看看他的博客。\n\n![xiaoming_blog](images/blog/xiaoming_blog.png)\n\n10，[SnailTyan blog](http://noahsnail.com/)\n\n深度学习开发者，通过作者的经典 backbone 论文翻译文章关注他的博客站点的，目前网站一直在坚持更新 leetcode 算法解题笔记。他的深度学习论文翻译系列文章很适合初学者学习下。\n\n![SnailTyan_blog](images/blog/SnailTyan_blog.png)\n\n11，[Lil'Log blog](https://lilianweng.github.io/archives/)\n\n国外不知名大佬，文章紧跟深度学习潮流，内容质量非常高，图文丰富，transformer、nas、object detection、gan、nlp、cv 等等，都有涉及，值得关注。\n\n![Lil'Log_blog](images/blog/Lil'Log_blog.png)"
  },
  {
    "path": "手把手教你注册和使用ChatGPT.md",
    "content": "- [一，何为 ChatGPT](#一何为-chatgpt)\n- [二，ChatGPT 注册步骤](#二chatgpt-注册步骤)\n  - [2.1，准备条件](#21准备条件)\n  - [2.2，注册虚拟电话号码](#22注册虚拟电话号码)\n  - [2.3，注册 OpenAI 账号](#23注册-openai-账号)\n- [三，使用 ChatGPT 官网服务](#三使用-chatgpt-官网服务)\n- [四，使用 ChatGPT App](#四使用-chatgpt-app)\n- [五，使用 ChatGPT Python API](#五使用-chatgpt-python-api)\n\n> 本文给出了 ChatGPT 的详细注册及使用教程，称得上是保姆级别的丰富图文教程。\n\n## 一，何为 ChatGPT\n\nChatGPT 是一个基于 GPT-3 模型的对话系统，它主要用于处理自然语言对话。通过训练模型来模拟人类的语言行为，ChatGPT 可以通过文本交流与用户互动。它可以用于各种场景，包括聊天机器人、智能客服系统等。基于 GPT-3 模型的对话系统通常会有很好的语言生成能力，并能够模拟人类的语言行为。\n\nChatGPT 虽然才发布几天时间，但是就已经火爆全网了，截止目前2022-12-8日，已经有开发者基于 ChatGPT 对话 api 接口开发了客户端聊天机器人、谷歌浏览器插件、 vscode 插件、微信群聊天问答机器人等衍生品。\n\n![image-20221208211954780](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172615299-408269154.png)\n\n![image-20221208212629979](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172618255-631726215.png)\n\n## 二，ChatGPT 注册步骤\n\n### 2.1，准备条件\n\n- 首先你必须要能**科学上网**（如何操作请自行查找资料）。\n\n- 最后，如果有一个国外手机号最好，没有的话就参考下文步骤去 sms-activate.org 网站注册一个虚拟号。\n\n注意！即使使用 VPN 科学上网，查看 `ip`地址也可能显示在国内，以下是解决办法:\n\n1. 代理要一定挂**全局**模式 + 支持 `OpenAI` 的国家（目前中（包括香港）俄等国不支持，推荐北美、日本等国家）；上面步骤完成后依然不行的话，就清空浏览器缓存然后重启浏览器或者电脑；\n2. 或者直接新开一个**无痕模式**的窗口。\n\n### 2.2，注册虚拟电话号码\n\n1，在  [sms-activate.org](https://sms-activate.org/cn) 网站注册一个账号并登录，默认是使用邮箱注册的。\n\n2，点击网站右上角的充值按钮，进入充值页面，选择支付宝，因为我们只需接受验证码服务，所以充值 `0.2` 美元即可。\n\n3，回到网站首页，在左侧搜索栏直接搜索 `openai` 虚拟手机号注册服务，可以随便选择一个国家号码，这里我选择印度 `India`，然后点击购物车会直接购买成功，然后就会进入你购买的虚拟手机号页面，复制去掉区号 `91` 的手机号码去 `openai` 官网注册服务即可。\n\n![image-20221208213417137](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172621334-733260879.png)\n\n\n\n![image-20221208214035545](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172624082-1339457343.png)\n\n![image-20221208215135904](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172627695-785439559.png)\n\n### 2.3，注册 OpenAI 账号\n\n前面的步骤完成后，我们就有一个虚拟的国外手机号码了，可以用来注册 openai 账号时完成**接收验证码**的服务。\n\n> OpenAI 网站注册必须要国外手机号码。\n\n最后，注册 `OpenAI` 账号，可以按照以下步骤进行：\n\n1. 点击[官网链接](https://link.zhihu.com/?target=https%3A//chat.openai.com/)，首次进入会要求你注册或登录账户。\n2. 使用谷歌、微软或者其他邮箱进行注册登录。\n3. 如果 `ip` 在国外，则能成功注册，然后就会看到填写用户名的页面，如果 `ip` 在国内就会提示你 `OpenAI's services are not available in your country`。\n4. **电话验证**。这个时候输入前面购买的虚拟手机号用来接收验证码即可。\n5. 通过电话验证之后，就能成功注册好 OpenAI 账号了，然后就去愉快的使用 `ChatGPT` 吧！\n\n<img src=\"./images/chatgpt/sigin_ip_openai.png\" alt=\"注册openai账号1\" style=\"zoom:33%;\" />\n\n## 三，使用 ChatGPT 官网服务\n> 注意：经常官网服务经常会拥挤，并出现如下提示，刷新重新进入即可。\n> \"We're experiencing exceptionally high demand. Please hang tight as we work on scaling our systems.\"\n\n1. 访问 [OpenAI 官网](https://openai.com/)，点击网页左边底部的 “Chat GPT”，进入 [ChatGPT 页面](https://chat.openai.com/chat)。\n2. 点击 “TRY CHATGPT”，进入 ChatGPT 服务页面。\n3. 在“Input”中输入你要和Chat GPT聊天的信息，然后点击“Send”。\n4. Chat GPT会根据你的输入回复一条信息，你可以根据回复的信息继续聊天。\n\n![chatgpt_network](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172637287-182827456.png)\n\n## 四，使用 ChatGPT App\n\ngithub 有个 stars 数目比较多的开源项目[ChatGPT](https://github.com/lencx/ChatGPT)，实现了将 ChatGPT 服务封装成 App 的功能，并支持 Mac, Windows 和 Linux 平台。\n\n这里以 Mac 系统的安装使用为例，其他平台类似，都很简单。\n\n1. 通过下载 [ChatGPT dmg安装包]( https://github.com/lencx/ChatGPT/releases/download/v0.10.3/ChatGPT_0.10.3_x64.dmg)方式直接双击安装。\n2. 通过 Homebrew 服务下载安装。\n\n```shell\nbrew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git\nbrew install --cask chatgpt --no-quarantine\n```\n\n下载安装 chatgpt app 后，打开软件会要求你先输入 openai 账户邮箱和密码，然后就可以直接使用了，App 界面如下所示。\n\n![chatgpt_app](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172647983-240591750.png)\n\n## 五，使用 ChatGPT Python API\n\n在前面的步骤完成注册 OpenAI 账户并申请 API Key 后，我们就可以愉快的玩耍 ChatGPT 了，这里通过 Python API 的形式调用 ChatGPT 服务。可以通过 [OpenAI 账户](https://platform.openai.com/account/org-settings) 找到自己 `API keys`，具体如下图所示。\n\n![api_key1](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172650691-1295296175.png)\n![api_key2](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172653586-165205223.png)\n\n然后创建以下 Python 代码并运行。\n\n```python\nimport openai\n\n# Set the API key\nopenai.api_key = \"YOUR_API_KEY\"\n\n# Define the model and prompt\nmodel_engine = \"text-davinci-003\"\nprompt = \"What is the capital of France?\"\n\n# Generate a response\ncompletion = openai.Completion.create(\n    engine=model_engine,\n    prompt=prompt,\n    max_tokens=1024,\n    n=1,\n    stop=None,\n    temperature=0.5,\n)\n\n# Get the response text\nmessage = completion.choices[0].text\n\nprint(message)\n```\n\n在上面的代码中，您需要将 `YOUR_API_KEY` 替换为您的 `API Key`，然后您可以运行代码并检查生成的输出。您可以通过更改提示文本和其他参数来生成不同的响应（回答）。\n\n![result](https://img2023.cnblogs.com/blog/2989634/202302/2989634-20230213172657025-1126630344.png)\n\n"
  }
]