2024年11月

Jave Web是java面向web开发的相关技术,他是相关技术的统称,并不是指某一个单一的技术。
在我之前的博客中(Java网络编程----通过实现简易聊天工具来聊聊BIO模型 https://www.cnblogs.com/jilodream/p/17405923.htm),就已经写到过java可以作为一个服务器(如TCP/UDP),接收外部的请求。如使用TCP监听端口,然后直接用web页面请求该端口,那么服务器就会接收到相关的响应,接着做好业务处理,返回响应的请求即可。但是整个的业务流程太繁琐了。我们不但要处理业务流程,还要控制请求会话,还要控制各种业务分支的处理,显然这不是我们想要的。
于是聪明的开发者很快想到了-----解耦,业务人员只要编写相关的业务即可,不需要关心繁琐的网络细节处理,因此就诞生了servlet。开发人员只要实现servlet,而servlet和不同的路径绑定。web请求后,由系统直接转发到各自的Servlet,并由Servlet来处理相关业务即可。
那什么是servlet呢?servlet 是
Server Applet
(服务器应用程序)的简写,从名字我们就可以看出它是专门用来编写服务器端的应用程序。我们通常用它来处理服务器中http请求的处理和响应。

它是一项很古老的技术,随java诞生之初就已经问世,很多人甚至都不知Servlet是做什么。那么问题来了,我们为什么还要学习和掌握Servlet呢?这主要是由于Servlet是javaEE的重要组成,是java web开发的重要基石。我们现在项目中常用到的Jsp、Springmvc、Springboot等框架,在处理网络请求的核心技术,仍然是Servlet。我们虽然不需要再继续直面Servlet或更底层的技术进行开发,但是Servlet究竟是什么样的,如何执行,以及再新技术中承担什么样的角色,这个却是我们想要熟悉底层原理所必须要掌握的。

话不多说,想要使用Servlet,我们需要做两步:

1、编写Servlet相关业务代码
2、将业务代码打包放置在Tomcat中,由Tomcat来加载这些Servlet

第一步,试着来编写一个Servlet
我们首先通过IDEA 新建一个web项目,此处我们选择采用maven部署,搭建好之后项目整体的结构就如下面的文件层级树一样

E:.
├─.idea
├─.smarttomcat
│ └─PureJaveServlet
│ └─conf
└─src
└─main
├─java
│ └─org
│ └─example
├─resources
└─webapp

其中:

resources 一般是我们填写的资源信息,如图片,业务配置文件等,
webapp则会放置html、css等渲染文件,还会放一些web组件(Servlet、Filter)的配置信息。我们这里只要知道作用即可。
src/main/java则是我们的业务代码。值得注意的是,我们要引入Servlet网络编程的相关依赖包,才能进行相关的web开发。默认的java SE是不包含这些开发包的。
在Servlet4.0之前,我们只使用javax相关的包,并且未来对接的是Tomcat 9.x及以下版本。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )
在Servlet5.0之后,我们只使用javax相关的包,并且未来对接的是Tomcat 10.x及以后版本。
这里我们使用高版本来学习,即jakarta版本,未来tomcat也需要使用高版本。

接着编写POM文件:

1 <?xml version="1.0" encoding="UTF-8"?>
2 <projectxmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6 
7 <groupId>org.example</groupId>
8 <artifactId>PureJaveServlet</artifactId>
9 <version>1.0-SNAPSHOT</version>
10 <name>PureJaveServlet</name>
11 <packaging>war</packaging>
12 
13 <properties>
14     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15     <maven.compiler.target>11</maven.compiler.target>
16     <maven.compiler.source>11</maven.compiler.source>
17 </properties>
18 
19 <dependencies>
20     <dependency>
21         <groupId>jakarta.servlet</groupId>
22         <artifactId>jakarta.servlet-api</artifactId>
23         <version>5.0.0</version>
24         <scope>provided</scope>
25     </dependency>
26 
27     <dependency>
28         <groupId>org.projectlombok</groupId>
29         <artifactId>lombok</artifactId>
30         <version>1.18.30</version>
31     </dependency>
32     <dependency>
33         <groupId>org.apache.commons</groupId>
34         <artifactId>commons-lang3</artifactId>
35         <version>3.13.0</version>
36     </dependency>
37     <!--http客户端-->
38     <!--fastjson-->
39     <dependency>
40         <groupId>com.alibaba</groupId>
41         <artifactId>fastjson</artifactId>
42         <version>1.2.83</version>
43     </dependency>
44 
45 
46     <dependency>
47         <groupId>com.fasterxml.jackson.core</groupId>
48         <artifactId>jackson-databind</artifactId>
49         <version>2.10.0</version>
50     </dependency>
51     <dependency>
52         <groupId>io.pebbletemplates</groupId>
53         <artifactId>pebble</artifactId>
54         <version>3.1.6</version>
55     </dependency>
56     <dependency>
57         <groupId>org.apache.maven.plugins</groupId>
58         <artifactId>maven-compiler-plugin</artifactId>
59         <version>3.10.1</version>
60     </dependency>
61 
62 </dependencies>
63 
64 <build>
65     <plugins>
66         <plugin>
67             <groupId>org.apache.maven.plugins</groupId>
68             <artifactId>maven-war-plugin</artifactId>
69             <version>3.3.2</version>
70         </plugin>
71         <plugin>
72             <groupId>org.apache.maven.plugins</groupId>
73             <artifactId>maven-compiler-plugin</artifactId>
74             <configuration>
75                 <compilerArgs>
76                     <arg>-parameters</arg>
77                 </compilerArgs>
78             </configuration>
79         </plugin>
80     </plugins>
81 </build>
82 </project>

Servlet类

1 packageorg.example;2 
3 
4 importjakarta.servlet.ServletException;5 importjakarta.servlet.annotation.WebServlet;6 importjakarta.servlet.http.HttpServlet;7 importjakarta.servlet.http.HttpServletRequest;8 importjakarta.servlet.http.HttpServletResponse;9 
10 importjava.io.IOException;11 importjava.io.PrintWriter;12 
13 
14 @WebServlet(urlPatterns = "/nihao")15 public class HiServlet extendsHttpServlet {16 @Override17     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throwsIOException {18         String name = req.getParameter("name");19         resp.setContentType("text/html");20         PrintWriter out =resp.getWriter();21         out.println("<html><body>");22         out.println(String.format("<h1>Hello, %s </h1>", name));23         out.println("</body></html>");24 out.flush();25 }26 }

1 packageorg.example;2 
3 importjakarta.servlet.annotation.WebServlet;4 importjakarta.servlet.http.HttpServlet;5 importjakarta.servlet.http.HttpServletRequest;6 importjakarta.servlet.http.HttpServletResponse;7 
8 importjava.io.IOException;9 importjava.io.PrintWriter;10 
11 /**
12 * @discription13  */
14 @WebServlet(urlPatterns = "/bye")15 public class ByeServlet extendsHttpServlet {16 @Override17     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throwsIOException {18         String name = req.getParameter("name");19         resp.setContentType("text/html");20         PrintWriter out =resp.getWriter();21         out.println("<html><body>");22         out.println(String.format("<h1>bye bye, %s </h1>", name));23         out.println("</body></html>");24 out.flush();25 }26 }

代码部分就结束了,我们观察代码可以发现两点:
1、所有的Sevlet都要继承自HttpServlet。HttpServlet是一个抽象类,我们需要复写抽象类中的抽象方法,以保证未来Tomcat等web服务器在加载Servlet时,可以按照统一的规范查找,执行。
我们在Servlet中重写了doGet() 方法,表示处理该servlet路径下的get请求,同理还可以重写doPost doDelete doPut等方法,来处理对应的请求类型。
这里我们很简单,直接返回一段响应的html。
2、我们并没有写main方法,而是在Pom中标记我们的工程需要打包成一个war包。
第二步,配置tomcat
没有main方法,我们如何启动我们的java程序呢?我们通常是将其配置到tomcat的指定路径中,启动tomcat后,tomcat会加载war包中servlet的相关类,进行处理。
因此我们会将tomcat这样的web服务器称之为Servlet容器。
我们首先从tomcat官网上(https://tomcat.apache.org/whichversion.html)下载一个与我们对应的servlet版本匹配的tomcat版本。

下载到本地之后解压即可。
接着我们为了方便在IDEA中下载一个smart tomcat的组件,将该组件关联好servlet代码和tomcat服务器即可。
关键配置如下:
Tomcat server: 选择我们下载好的tomcat服务器,如果没有下拉选项就选择"Congure..."手动加一下。
Deployment dirctory:部署文件夹,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )该配置指向前文项目结构树种的webapp。
Use classpath of module:选择当前项目
Context path:选择上下文路径(其实就是url的前缀路径),按照url规则随便填,我这里叫填的是/biubiubiu
server port:Servlet的服务器端口,默认填8080
admin port:tomcat的管理端口 默认填8005,一般就是用于停掉tomcat(其实一般也不用)。
之后我们通过IDEA:拉起tomcat,加载servlet相关类和资源。

命令行输入如下:

....
15-Nov-2024 10:34:38.099 信息 [main] org.apache.coyote.AbstractProtocol.start 开始协议处理句柄["http-nio-8080"]15-Nov-2024 10:34:39.831 信息 [main] org.apache.catalina.startup.Catalina.start [4858]毫秒后服务器启动
http:
//localhost:8080/biubiubiu

执行效果如下:

有人会觉得通过下载并配置tomcat有点麻烦,我们如果想debug代码的话,就更麻烦了,有没有简单点的办法:
其实除了下载tomcat,我们还可以通过代码的形式直接拉起tomcat。思路如下:
首先通过maven加载对应tomcat依赖,然后在main方法中创建tomcat实例,并且指定tomcat所需要的配置信息,如资源和class路径。然后通过start()方法启动tomcat实例就可以了。
代码如下:

新搭建一个java web项目,Servlet类和工程结构我们保持不变还是和原来一样

POM文件

1 <?xml version="1.0" encoding="UTF-8"?>
2 <projectxmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5     <modelVersion>4.0.0</modelVersion>
6 
7     <groupId>com.example</groupId>
8     <artifactId>demotom</artifactId>
9     <version>1.0-SNAPSHOT</version>
10     <name>demotom</name>
11     <packaging>war</packaging>
12 
13     <properties>
14         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15         <maven.compiler.target>11</maven.compiler.target>
16         <maven.compiler.source>11</maven.compiler.source>
17         <tomcat.version>10.0.0</tomcat.version>
18     </properties>
19 
20     <dependencies>
21 
22 
23         <dependency>
24             <groupId>org.apache.tomcat.embed</groupId>
25             <artifactId>tomcat-embed-core</artifactId>
26             <version>${tomcat.version}</version>
27             <scope>provided</scope>
28         </dependency>
29         <dependency>
30             <groupId>org.apache.tomcat.embed</groupId>
31             <artifactId>tomcat-embed-jasper</artifactId>
32             <version>${tomcat.version}</version>
33             <scope>provided</scope>
34         </dependency>
35     </dependencies>
36 
37     <build>
38         <plugins>
39             <plugin>
40                 <groupId>org.apache.maven.plugins</groupId>
41                 <artifactId>maven-war-plugin</artifactId>
42                 <version>3.3.2</version>
43             </plugin>
44         </plugins>
45     </build>
46 </project>

主类:

1 packagecom.example.demotom;2 
3 importorg.apache.catalina.Context;4 importorg.apache.catalina.LifecycleException;5 importorg.apache.catalina.WebResourceRoot;6 importorg.apache.catalina.startup.Tomcat;7 importorg.apache.catalina.webresources.DirResourceSet;8 importorg.apache.catalina.webresources.StandardRoot;9 
10 importjava.io.File;11 
12 /**
13 * @discription14  */
15 public classTomMain {16     public static void main(String[] args) throwsLifecycleException {17         Tomcat tomcat = newTomcat();18         tomcat.setPort(Integer.getInteger("port", 8080));19 tomcat.getConnector();20         String docBase = new File("src/main/webapp").getAbsolutePath();21         Context ctx = tomcat.addContext("", docBase);22         WebResourceRoot resources = newStandardRoot(ctx);23         String base = new File("target/classes").getAbsolutePath();24         resources.addJarResources(new DirResourceSet(resources, "/WEB-INF/classes", base, "/"));25 ctx.setResources(resources);26 tomcat.start();27 tomcat.getServer().await();28 }29 }

启动后控制台输出如下,我们可以看到8080端口已经被监听:

"C:\Program Files\Java\jdk-11\bin\java.exe" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:51854,suspend=y,server=n -Dfile.encoding=UTF-8 -classpath ....
Connected to the target VM, address: '127.0.0.1:51854', transport: 'socket'
11月 15, 2024 10:47:54 上午 org.apache.coyote.AbstractProtocol init
信息: Initializing ProtocolHandler ["http-nio-8080"]
11月 15, 2024 10:47:54 上午 org.apache.catalina.core.StandardService startInternal
信息: Starting service [Tomcat]
11月 15, 2024 10:47:54 上午 org.apache.catalina.core.StandardEngine startInternal
信息: Starting Servlet engine: [Apache Tomcat/10.0.0]
11月 15, 2024 10:47:56 上午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
警告: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [1,753] milliseconds.
11月 15, 2024 10:47:56 上午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-nio-8080"]

在深度学习的领域中,前向传播、反向传播和损失函数是构建和训练神经网络模型的三个核心概念。今天,小编将通过一个简单的实例,解释这三个概念,并展示它们的作用。

前向传播:神经网络的“思考”过程

前向传播是神经网络计算的基础步骤,它涉及将输入数据逐层传递,通过神经网络的权重和激活函数,最终输出预测结果。这个过程包含
“样本数据输入、算法模型、输出”
这几个步骤。

我们来举个简单的例子,比如给一个小宝宝看一张图片,然后问他:“这上面画的是什么?”他就会用他的小脑袋瓜去“思考”这张图片,然后告诉你答案。前向传播就像是这个过程,只不过小宝宝换成了神经网络。

  • 样本数据输入
    :这一步将图像、文字、语音等样本数据转换为我们电脑能识别的数字输入。就像小宝宝看到图片,神经网络也接收到一张图片,这张图片被转换成一串数字。
  • 算法模型
    :简单来说,就是一些数学计算,主要包含线性层+规则化层+激活,线性层负责做线性函数的拟合;规则化层负责把我们的线性拟合规则化,方便后面的计算;激活层负责的是变成非线性化,因为我们的现实世界是非线性的。所以整个过程就是:我们输入的样本是非线性的,我们通过这样一堆数学公式,去拟合非线性的样本数据。
  • 输出层
    :也是一些数学运算,比如Linear或者Conv,负责将模型的输出转换为预测结果输出。

这个过程可以用下面的数学公式表示:
image

损失函数:告诉神经网络它错了多少

损失函数是衡量模型预测结果与真实标签之间差距的依据,它的核心作用是告诉我们模型的预测结果“错”得有多离谱。通俗来说,
损失函数就像是一个裁判,它给模型的预测结果打分,分数越低,说明模型的预测结果越接近真实情况,模型的性能就越好
。损失函数是为了让我们反向传播起作用的。就像如果小宝宝猜错了,你会告诉他:“不对哦,这是数字8,不是3。”损失函数就像是这句话,它告诉神经网络:“嘿,你的答案有点偏差。”

下面是几种常用的损失函数:

L1 Loss(MAE)
:平均绝对误差,对异常值的容忍性更高,但当梯度下降恰好为0时无法继续进行。就像是你告诉小宝宝:“你的答案差了多远。”这个距离就是损失值。
image

L2 Loss(MSE)
:均方误差,连续光滑,方便求导,但易受到异常值的干扰。这就像是你告诉小宝宝:“你的答案差了多少个单位。”这个单位的平方和就是损失值。
image

Smooth L1 Loss
:处理异常值更加稳健,同时避免了L2 Loss的梯度爆炸问题。就像是你告诉小宝宝:“你的答案差了多远,但我不会因为你猜得特别离谱就惩罚你。”这个损失函数对极端错误更宽容。
image

反向传播:神经网络的“自我修正”过程

反向传播是利用损失函数的梯度来更新网络参数的过程。它从输出层开始,逆向通过网络,利用链式法则计算每个参数对损失函数的梯度。包含这几个过程:

  • 计算输出层误差梯度
    :首先计算输出层的误差梯度,这是损失函数对输出层权重的敏感度。
  • 逐层反向传播
    :然后从输出层开始,逆向通过网络,逐层计算误差梯度。
  • 更新权重和偏置
    :使用梯度下降算法,根据计算出的梯度更新网络中每一层的权重和偏置。

所以前向传播、反向传播、损失函数之间的关系是这样的:

他们都是深度学习训练过程中的核心。
前向传播
负责生成预测结果,
损失函数
负责量化预测结果与真实标签之间的差异,而
反向传播
则负责利用这些差异来更新模型参数,以减少损失函数的值。

通过三者的结合,我们可以构建、训练并优化深度学习模型,使其能够从数据中学习复杂的模式,并在各种任务如图像识别、自然语言处理和预测分析中做出准确的预测。

前向传播、反向传播、损失函数属于机器学习领域中的核心概念,在AI全体系课程中,是理解其他更复杂机器学习算法的基础,掌握这几个概念对于深入学习机器学习、理解更高级的算法以及在实际应用中设计和优化模型都具有重要的意义。通过理解前向传播、反向传播和损失函数,学习者能够更好地把握机器学习模型的工作原理,为进一步探索深度学习和其他高级机器学习技术打下坚实的基础。

我们在进行页面跳转时,很多情况下都得考虑登录状态问题,比如进入个人信息页面,下单交易页面等等。在这些场景下,通常在页面跳转前,会先判断下用户是否已经登录,若已登录,则跳转到相应的目标页面,若没有登录,则先跳转到登录页面,然后等着获取登录状态,若登录页面关闭时,能获取到已登录,则继续跳转到目标页,若用户取消了登录,则终止后面的行为。这样的处理通常会存在一些问题,例如很多页面都与登录状态相关,这样需要在大量的入口处增加登录逻辑判断。即使封装成一个方法,也需要关心是否登录成功,增加了逻辑的复杂性,而且登录页面先关闭,再打开新页面,页面切换动画也很不协调。

那么我们有没有一种更好的方案来处理登录鉴权问题呢?首先我们先梳理一下我们想要的效果,我们的目的是要跳转到相应的目标页,目标页是否需要先登录,我们是不太愿意关注的,最好是内部自己处理掉,,若没有登录,就先进行登录,登录成功后,继续后面的行为,外面使用的地方尽量做到无感知。总结一下就是进行页面跳转时,内部先判断一下状态,然后再进行后续的行为,而这恰好是Navigation拦截器的功能。

NavPathStack提供了setInterception方法,用于设置Navigation页面跳转拦截回调。该方法需要传一个NavigationInterception对象,该对象包含三个回调函数willShow,didShow和modeChange,我们在willShow页面即将显示时,进行拦截处理。先判断是否登录,没有登录,就重定向到登录页面,若已登录,则继续后续行为,不做拦截。示例如下

@Entry
@ComponentV2
struct Index {
  nav: NavPathStack = new NavPathStack()
  isLogin: boolean = false

  aboutToAppear(): void {
    this.nav.setInterception({
      willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
        operation: NavigationOperation, isAnimated: boolean) => {
        if (typeof to === 'object') {
          if (isLogin) {
            AppRouter.popPage()
            AppRouter.jumpPage('login', undefined)
          }
        }
      }
    })
  }

  build() {
    Navigation(this.nav)
    .hideToolBar(true)
    .hideTitleBar(true)
    .height('100%')
    .width('100%')
  }
}

拦截器细节优化

如何判断是否需要进行拦截

在拦截器中,虽然我们可以进行拦截重定向跳转,但需要考虑的一个问题是什么情况下进行拦截,也就是哪些页面跳转时需要先判断下登录状态。首先想到的是弄一个数组,所有需要登录校验的页面都放到这个数组中。页面跳转时,我们只需要判断下目标页是否在数组中,就可以知道是否需要进行拦截校验登录了。其实思想是对的,只是我们有更简单的实现方式。在系统路由表中,有一个data字段,可以在这个字段中增加一个字段,是否需要登录,在拦截器中先获取目标页中这个参数,只要所有需要登录的页面,都添加了这个字段就可以了。我们以用户信息页为例,配置如下

{
  "routerMap": [
    {
      "name": "login",
      "pageSourceFile": "src/main/ets/pages/login/LoginPage.ets",
      "buildFunction": "loginBuilder"
    },
    {
      "name": "user_info",
      "pageSourceFile": "src/main/ets/pages/user/UserInfoPage.ets",
      "buildFunction": "userInfoBuilder",
      "data": {
        "needLogin": "1"
      }
    }
  ]
}

拦截器中获取该字段的方式如下

this.nav.setInterception({
  willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
    operation: NavigationOperation, isAnimated: boolean) => {
    if (typeof to === 'object') {
      const data = (to as NavDestinationContext).getConfigInRouteMap()?.data
      if (data !== undefined && (data as object)['needLogin'] === '1' && !AppConstant.hasLogin) {
        AppRouter.popPage()
        AppRouter.jumpPage(Pages.login, undefined)
      }
    }
  }
})

登录成功后如何获取目标页和页面参数

登录成功后,我们如何知道要跳转到哪个目标页,以及跳转到目标页时所需要的参数呢?我们在跳转到登录页时可以增加2个参数targetPage和targetParam,分别表示要处理的目标页以及相应的参数,若targetPage的值为undefined,则说明登录成功后没有后续操作,若有值,则跳转到这个页面并把相应的参数传过去。在拦截器中,可以通过to.pathInfo.name获取到目标页的名称name以及通过to.pathInfo.param获取到目标页所需要的参数,并把它们赋值给登录页面的targetPage和targetParam就行了。

我们可以发现使用拦截器这种方式,完全符合我们最初的设想,外部调用时不用考虑是否要校验登录状态,由拦截器内部自己处理。登录后也是直接跳转到目标也,没有页面关闭效果。而且是否需要判断登录,只需配置一个字段就行了,非常方便。

汇编中,加法指令很重要,因为它是执行其他很多指令的基础。

同时,加法指令也会影响
NZCV
标志。有关
NZCV
的介绍,可以参看《一文搞懂 ARM 64 系列: ADC》。

ARM64
汇编中,
ADD
指令有
3
种形式,这里介绍第一种形式,也就是与
立即数
相加。

1 指令语法

ADD <Xd|SP>, <Xn|Sp>, #imm{, shift}

{}
里的内容表示是可选的。

shift
表示
LSL(逻辑左移)
的位数,有
2
个取值,一个是
0
,一个是
12

0
是其默认值。

所谓
LSL(逻辑左移)
,是指将数值整体向左移动,低位补
0
。如果高位被移出去,直接丢弃。

image

2 指令语义

整个指令就是将源寄存器
<Xn|SP>
,与立即数
imm
(如果有必要,需要进行
LSL
)相加,将结果写入目的寄存器
<Xd|SP>

注意
,这条指令不影响
NZCV
标志。

(<Xd|SP>, _) = <Xn|Sp> + imm << shift

3 NZCV 如何受影响

虽然这条指令最终不影响
NZCV
标志,但是搞清楚
NZCV
如何受影响,还是很有必要的。

1
将源寄存器的值
<Xn|SP>

imm << shift
都当成
无符号整型数
,两数相加,得到一个
无符号整型数
的结果,记作
u_result
。此时计算时不考虑溢出:

<Xn|SP> = 0xffffffffffffffff // 64 bit 全 1
(imm << shift) = 1
u_result = 0xffffffffffffffff + 1 = 0x10000000000000000 // 2^64,而不是 0

2
将源寄存器的值
<Xn|SP>

imm << shift
都当成
有符号整型数
,两数相加,得到一个
有符号整型数
的结果,记作
s_result
。此时计算时不考虑溢出:

<Xn|SP> = 0xffffffffffffffff // 64 bit 全 1,此时当成 -1 看待
(imm << shift) = 0x8000000000000000 // 64 bit 最小负整数 -9223372036854775808
s_result = -1 + (-9223372036854775808) = -9223372036854775809 // 而不是 0x7fffffffffffffff

3

u_result
中取
(63~0)

64bit
,记作
result
;

4
如果
result
的最高位是
1
,那么
N = 1
;

5
如果
result = 0
,那么
Z = 1
;

6
如果把
result
当成
无符号整型数
,它的值等于
u_result
,那么
C = 0
;如果不等于,那么
C = 1
,也就是在进行加法运算时,发生了进位。

7
如果把
result
当成
有符号整型数
,它的值等于
s_result
,那么
V = 0
;如果不等于,那么
V = 1
,也就是说在进行加法运算,发生了溢出。

最近大家都在探讨和尝试复现OpenAI O1的思考效果,解码出的关键技术方向,包括之前已经探讨过的Inference Time Scaling在推理过程中进行路径决策和选择。但想要更优的Inference Time Scaling曲线,前提是模型本身是一个很强的Generator,已经拥有足够的生成合理推理过程的能力,同时还拥有很强的Verifier模型来对推理节点进行打分决策,并且二者可以在少人类监督的条件下不断迭代优化。

这一章我们先聊聊如何让大模型"自学"推理思考,从而得到思考推理能力更强的Generator。本章会以STaR论文为基础,介绍生成复杂动态思维链背后可能的技术方案

STaR

  • STaR: Self-Taught Reasoner Bootstrapping ReasoningWith Reasoning

STaR是这一系列论文的第一篇,思路就是妥妥的Bootstrap,生成推理过程->训练模型->生成更优的推理过程->训练更强的模型。

STaR的流程很直观

  • Pretrain模型,通过指令+fewshot,引导模型对QA数据集生成推理过程
  • 对以上推理过程进行过滤,只保留回答正确的
  • 对推理答案错误的,通过Hint(在上文中告诉模型正确答案),引导模型生成正确的推理过程,对这部分样本也进行过滤,只保留回答正确的
  • 使用以上样本进行SFT,教模型如何思考
  • 再使用SFT后的模型重复以上样本生成的过程,直到评估指标不再提升

STaR的优缺点都非常明显,优点就是不需要大量人工标注的思维链样本,也不依赖更强大的模型提供合成样本(其他模型提供的合成样本本身也可能存在分布漂移会影响模型效果),实现了一定程度的模型自我优化提升。缺点有

  • 可用场景有限:STaR依赖正确答案作为过滤条件,因此只适用于问答,数学计算等有限领域,对于更广泛的开放领域无法适用。这个限制其实也是因为STaR并未引入Verifier,因此只能依赖答案本身作为评估基准。
  • SFT本身的泛化性有限:通过SFT把生成的推理过程注入模型,很难让模型学到推理过程中的奖励信号,更多还是在做Behaviour Cloning。达不到"Don't Teach, Incentive"的效果
  • STaR对样本的使用率不足,只使用了唯一的一条正确样本,丢弃了通往正确答案的更多正确路径,也丢弃了更大量级的错误思考过程
  • 思考链路是静态,既针对任何问题模型都默认上来就进行思考,这种形式在单一场景中适用,在更灵活广泛的实际场景中思考应该动态存在

下面我们看下针对以上问题,其他论文给出了哪些优化方案,以下论文更多会关注和STaR的对比~

RFT

  • Scaling relationship on learning mathematical reasoning with large language models

RFT也是模型自我合成数据进行优化的方案,它没有使用STaR的多轮Bootstrap来持续优化合成数据,只用了一轮优化,但RFT给出了在一轮迭代内,更充分利用正样本的方案。

RFT会使用SFT后的模型,针对每个问题随机采样100条推理路径,筛选所有答案正确的推理路径,并使用编辑距离对不同的推理路径进行消重,只保留差异化的正确推理路径。这样对比以上STaR每个问题只有1条正确样本,RFT对每个问题会保留多样性的正确推理路径,然后使用该合成数据集对模型进行训练。对比后发现使用更多推理路径效果会有提升,同时去重也会带来明显的效果提升。大概率因为不去重,会导致部分重复样本的过度拟合,影响泛化性。

RFT这种使用模型自我合成数据再微调基座的方案,在后面Google Deepmind的论文中也进一步论证了它的有效性要超过使用更强大的模型直接合成数据的效果。部分因为多个正确推理路径的提供,能给模型提供一些哪些推理节点是核心节点的有效信息,降低模型模仿率,提高模型泛化性。
image

V-STaR

  • V-STaR: Training Verifiers for Self-Taught Reasoners

V-STaR沿用了STaR的多轮Bootstrap样本迭代的方案,并给出了一种简单的利用负样本的方案,在以上STaR的基础上,每一轮模型生成推理答案时,正确和错误的推理链路都会被保留,其中正确的样本用来训练微调Generator,而正确和错误的样本会合并用于训练Verifier。

以及和STaR每一轮都只使用新训练的Generator合成的样本不同,这里训练Verifier的样本是每一轮收集样本的并集。因为RM模型需要广泛学习不同分布的推理结果,而每一轮随着Generator不断增强,其实都在拓宽RM模型学习的样本范围,提升Verifier的泛化性。

最后论文用收集好的正负样本,构建了针对问题的对比样本对(x, y+,y-) ,然后使用DPO在最后一轮微调得到的最优的Generator上来训练Verifier。并在推理过程中使用该Verifier,来实现best-of-n策略,从N个随机采样的推理结果中选择RM得分最高的推理链路。

image

效果上加入Verifier的STaR效果会有进一步提升,并且多轮Bootstrap也能有效提高V-STaR的效果。

Incorrect Synthetic Data

  • RL on Incorrect Synthetic Data Scales the Efficiency of LLM Math Reasoning by Eight-Fold

GDM这篇论文对正负合成思维链样本都做了更加全面的讨论,基本结论如下

  • 正样本:论文论证了前面RFT,也就是使用微调模型自我生成推理链路的方案,要优于使用更强模型直接生成样本进行SFT。但是只使用合成正样本做SFT,因为无法保证链路的完全正确,会让模型学到一些混淆的错误思考模式。
  • 负样本:对比V-STaR只在Verifier中简单利用了负样本,论文给出了在优化Generator中使用负样本的训练方案

下面我们分正负样本来分别说下~

正样本:为何自我生成的正样本效果更好?

论文分别采用两种方案来合成数据

  • SFT:使用更强大的模型合成数据,例如GPT4来生成带有思维链的推理样本,经过简单的消重,过滤错误答案后,使用正确样本直接微调模型
  • RFT:模型自我合成数据,使用以上微调后的模型,针对每个问题再生成N个推理结果,经过过滤后使用正确的样本微调模型,也就是使用基座微调模型自我生成的样本再回来微调基座

论文发现在Deepseek和Llama2上,随着合成数据集的数量变大,RFT显著优于SFT,并且优势并不随数据集变大而缩小。具体到
数据使用效率,相同的Test Error下,使用RFT策略训练的效果相当于使用2倍的合成数据进行SFT

image

这个结论会有一些反直觉,因为之前很多优化小模型的思路都是去蒸馏GPT4的回答。当然后面也有一些研究认为
拟合另一个模型的回答,因为预训练的差异,导致微调过程中模型很难直接学习新的推理回答只能强行记忆,影响模型泛化效果。
类似的问题其实在早期我们也用GPT3.5,GPT4的回答去构建样本,然后微调一些小模型的时候就发现了,当回答风格差异巨大的时候,直接微调,会影响基座本身的知识存储和指令理解。其实就是小模型为了去强行改变自己的输出风格,负向影响了模型本身的参数分布。

论文使用RFT生成的样本,相比SFT样本,在基座模型上有更高的log likelihood来论证之所以使用RFT的样本微调效果更好,就是因为
RFT样本是基座模型自我合成的,因此和基座模型本身的推理分布更加接近,模型更好学习,会降低模型去强行记忆的概率,对泛化性的损失更小,更加“easy-to-fit”。

但不论是SFT还是RFT,论文提出都需要关注
正确样本中错误的推理链路
,因为样本过滤只使用了答案,并未对中间推理链路的正确性进行校验,而这些错误的步骤,会导致模型学到一些混淆的因果关系。而
虚假步骤带来的推理问题,并无法通过简单的增加合成数据的方法来解决。

下面我们接着看论文如何通过引入负样本和per-step DPO来优化合成样本中错误步骤带来的问题。

负样本:呦呵你没想到我也这么有用吧

既然同一个问题生成多条正向的推理链路的合成样本可以提升效果,那如何更有效的利用比正样本占比更高的负样本呢?前面V-STaR是选择利用负样本去训练Verifier,而GDM的论文给出了通过
正负样本对比学习
来充分利用负样本的方案。论文设计的RL目标函数如下,通过正负样本分别和基准(微调后的基座模型)模型对比,来进行对齐。

image

并且论文给出了从“
关键步骤
”这个概念出发构建正负样本对的方案,那啥叫关键步骤嘞?

可以从熵值的视角去看,如果生成步骤A后,模型得到正确答案,或者错误答案的概率显著更高,那步骤A就是关键步骤。其中通往错误的核心步骤需要模型遗忘,通过正确的核心步骤需要学习。

那如果生成步骤A后,模型得到正确和错误答案的概率一半一半,那步骤A就不是关键步骤。想要获得每个步骤通往正确、错误答案的概率,其实只需要通过蒙特卡洛模拟采样足够多的链路,然后做个概率统计就行嘞

以上的
关键价值
,论文用以下的公式来计算,每个步骤(i)的价值(Q value),都是给定(1
i-1)的步骤,计算步骤模型在未来(i+1
L)步内获得正确答案的期望概率。以上价值其实是步骤(1~i)的累计价值,而每个步骤的增量价值,就是和截止前一步Q value的差值。

image

image

所以构建正负推理链路的步骤,就是基于每个问题,使用微调后的基准模型采样N条推理链路,基于答案是否正确,先初步分成正负样本。然后在负样本内,寻找每个推理链路中关键价值最低的第一个步骤(大概率导致模型失败的第一个核心节点)作为
\(-y_{1:c}\)
。在正样本内,寻找每个推理链路中关键价值的第一个步骤作为
\(+y_{1:c}\)
,然后基于这些正负样本对,使用以上对比RL损失函数进行训练。

效果上对比SFT,per-step DPO训练的效果,会发现
相同Test-Error下,使用per-step DPO训练,相当于使用8倍的合成数据进行SFT, 也就是per-step DPO的合成数据使用效率比SFT要高8倍
对比前面RFT,利用负样本,识别关键步骤进行对比学习,对合成数据有更高的利用率~

image

Quiet-STaR

Quiet-STaR拓展了以上STaR们的应用范围,针对开放世界的各类问题,给出了让模型先给出多条思考路径,再在推理时动态决策多大程度上依赖前置思考来回答问题的新方案。

image

Quiet-STaR的实现包括3步,我们对照的着下面的代码来说

  1. N路平行思考生成:针对每个输入token,生成N个推理链路,每个推理链路的长度都是固定的,并且在推理链路的开始和结束,分别插入新的Special token <startofthought>,<endofthought>。 这些推理链路就是模型的隐藏思考步骤。对应以下代码中continuation迭代生成的部分。
  2. 混合权重头:论文在原生LLM的基础上添加了一个分类头,就是简单一层或多层MLP。分类头用于进行权重混合,也就是在预测真实的下一个token的时候,根据分类头的权重,决定多少信息来自上一个输入token,多少信息来自新生成的隐藏思考步骤。对应以下代码中的mixing_weight和mixed_hidden_states。
  3. RL训练优化思考生成:最后通过强化学习,在训练以上<startofthought>,<endofthought>,MLP分类层的基础上,让模型生成的隐藏思考步骤更加有用,可以提高模型推理效果。
# Append the start thought token to the input sequence
start_thought_token_id = self.tokenizer.convert_tokens_to_ids("<|startthought|>")
input_ids = torch.cat([input_ids, torch.tensor([[start_thought_token_id]] * batch_size).to(input_ids.device)], dim=-1)
seq_len += 1

# Update the attention mask
if attention_mask is not None:
    attention_mask = torch.cat([attention_mask, torch.ones((batch_size, 1)).to(attention_mask.device)], dim=-1)

# Generate the continuation
continuation_length = self.n_ahead - 2
new_key_values = past_key_values

start_time = time.time()
for continuation_idx in range(continuation_length):
    outputs = self.model(
        input_ids=input_ids if continuation_idx == 0 else next_token_id.unsqueeze(-1).to(input_ids.device),
        attention_mask=attention_mask,
        position_ids=position_ids,
        past_key_values=new_key_values,
        inputs_embeds=inputs_embeds,
        use_cache=True,
        output_attentions=output_attentions,
        output_hidden_states=output_hidden_states,
        return_dict=return_dict,
    )
    new_key_values = outputs.past_key_values

    hidden_states = outputs[0]

    logits = self.lm_head(hidden_states)
    logits = logits[:, -1, :]  # Only consider the last token

    # Apply Gumbel-Softmax to the logits
    next_token_logits = F.gumbel_softmax(logits, tau=self.gumbel_temperature, hard=True, dim=-1)
    next_token_id = torch.argmax(next_token_logits, dim=-1)

    # Append the generated token to the input sequence
    input_ids = torch.cat([input_ids, next_token_id.unsqueeze(-1).to(input_ids.device)], dim=-1)
    seq_len += 1

    # Update the attention mask
    if attention_mask is not None:
        attention_mask = torch.cat([attention_mask, torch.ones((batch_size, 1)).to(attention_mask.device)], dim=-1)

# Append the end thought token to the input sequence
end_thought_token_id = self.tokenizer.convert_tokens_to_ids("<|endthought|>")
input_ids = torch.cat([input_ids, torch.tensor([[end_thought_token_id]] * batch_size).to(input_ids.device)], dim=-1)
seq_len += 1

# Update the attention mask
if attention_mask is not None:
    attention_mask = torch.cat([attention_mask, torch.ones((batch_size, 1)).to(attention_mask.device)], dim=-1)

# Get the hidden states before and after the thought
outputs_before = self.model(
    input_ids=original_input_ids,
    attention_mask=original_attention_mask,
    position_ids=position_ids,
    past_key_values=past_key_values,
    inputs_embeds=inputs_embeds,
    use_cache=use_cache,
    output_attentions=output_attentions,
    output_hidden_states=output_hidden_states,
    return_dict=return_dict,
)
hidden_states_before = outputs_before[0][:, -1:, :]

# two new tokens: last continuation token and end thought token
outputs_after = self.model(
    input_ids=torch.cat([next_token_id.unsqueeze(-1).to(input_ids.device), torch.tensor(end_thought_token_id).unsqueeze(-1).unsqueeze(-1).to(input_ids.device)], dim=-1),
    attention_mask=attention_mask,
    position_ids=position_ids,
    past_key_values=new_key_values,
    inputs_embeds=inputs_embeds,
    use_cache=use_cache,
    output_attentions=output_attentions,
    output_hidden_states=output_hidden_states,
    return_dict=return_dict,
)
hidden_states_after = outputs_after[0][:, -1:, :]

# Apply the talk head to get the mixing weight
mixing_weight = self.talk_head[0](torch.cat([hidden_states_before, hidden_states_after], dim=-1))

# Apply the mixing weight to the hidden states
mixed_hidden_states = (1 - mixing_weight) * hidden_states_before + mixing_weight * hidden_states_after

# Apply the language model head to get the final logits
logits = self.lm_head(mixed_hidden_states)

按照以上的步骤,我们来举个例子,输入是“今天天气“:

1. 平行思考生成(N=5)
  - 思考1:<startofthought>是否下雨、下雪等异常天气<endofthought>
  - 思考2:<startofthought>温度、湿度情况<endofthought>
  - 思考3:<startofthought>湿度情况<endofthought>
  - 思考4:<startofthought>对人们生活活动的影响<endofthought>
  - 思考5:<startofthought>穿衣建议<endofthought> 
2. 权重混合(思考1举例)
  - 无思考时输入的next-token:很(0.4)+ 真(0.6)
  - 思考1后面的next-token:晴朗(0.2) + 下雨(0.2) + ....各种天气情况
  - 思考1混合后(混合权重0.5):很(0.2) + 真(0.3)+晴朗(0.1) +下雨(0.1)+ ....各种天气情况*0.5
3. RL训练(如果原始文本后面是“晴朗”)
  - RL给予“思考1”以正向梯度更新,在天气后面思考温度有助于模型推理

论文给出的一个实际的推理效果如下
image

再来说下训练的部分,论文采用了强化学习来训练新加入的MLP混合头,思考开始和结束的token,以及基座模型本身。因为前面再每个位置生成了N个不同的思考路径,论文选择的RL训练目标是优化每个token位置生成正确思考路径,相对其他思考路径对推理下一个token带来的增量信息。
image

然后鼓励模型去生成对推理下一个token有帮助作用的思考路径(Tj)

image

同时还会增加一个NLI Loss,来训练用于混合思考和推理权重的MLP头。训练对比前面STaR等策略都是使用QA等特定领域指令样本,Quiet-STaR选择OpenWebMath(技术网站爬虫数据)进行训练,考虑技术类文本依赖思考的情况更多,模型训练得到的正向信号会更加密集。因为训练本身是预训练的Next-Token-Prediction,因此对比STaR具有更好的泛化效果,可以不限领域,不限任务进行训练。

Quiet-STaR还有待进一步优化的问题包括

  • 动态思考位置的选择:Quiet-STaR是在每个位置都生成N个思考链路后,再使用mix-head来对每个位置的思维链和原始推理进行权重融合,属于后选择方案,推理成本较高,如果能根据输入本身进行前置的思考位置选择,只在最优的一个或几个位置上进行内生思考推理(MCTS)就更完美了
  • 模型内容思考可能本身不可解释,因为Quiet-STaR只在HighLevel层面去优化加入内生思考后,模型推理效果的提升,并未对思考本身的next-token prediction进行对齐,导致生成的思考本身甚至可能并不在语言上通顺。当然因为本身是在训练后的基座模型上推理,所以肯定保留了部分的语言逻辑性
  • 模型内生思考可能存在各种3H(helpful,harmless,honesty)问题。同样是对齐问题,模型生成的思考链路不仅未在语言模型角度对齐,也未在人类偏好角度对齐,这可能也是OpenAI在O1中考虑对用户隐藏内在思考链路的原因之一。而对齐本身是否会影响内生思考的效果需要额外的实验验证。

Quiet-STaR和OpenAI O1在生成模型内生思考上的技术栈是很像的。OpenAI在O1的使用说明
Link
中也指出,O1是通过动态插入思考token,来生成内生思考,并基于内生思考进行推理回答,思考对用户不可见(OpenAI在
Learning to Reason with LLMs
中也说明隐藏思维链的部分是未对齐的),只展示回答部分。而多轮对话的上文也只会使用输入输出不会使用内生回答。使用感受上在金融场景下,一些强数字,强逻辑的问题例如表格问答,财务问题分析上O1有比较显著的效果提升。

image

想看更全的大模型论文·微调预训练数据·开源框架·AIGC应用 >>
DecryPrompt


OpenAI O1技术路线解析的一些好文推荐~

  1. OpenAI Learning to Reason with LLMs
  2. 北大对齐团队独家解读:OpenAI o1开启「后训练」时代强化学习新范式
  3. Reverse engineering OpenAI’s o1
  4. OpenAI’s Strawberry, LM self-talk, inference scaling laws, and spending more on inference
  5. OpenAI o1 self-play RL 技术路线推演
  6. 让 LLM 下一盘大棋:RL 范式探讨