2024年11月

Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)

https://cli.vuejs.org/zh/

使用脚手架

  1. 安装脚手架

    npm install -g @vue/cli
    
  2. 使用脚手架创建一个项目

    vue create vue-demo
    
  3. 进入目录启动服务

    npm run serve
    

脚手架结构

├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
├── vue.config.js:脚手架配置文件
// 默认的main.js
// 整个项目的入口文件

// 引入vue
import Vue from 'vue'
// 引入App组件,所有组件的父组件
import App from './App.vue'

// 关闭vue的生产提示
Vue.config.productionTip = false

// 创建vue实例对象 vm
new Vue({
  // 将App组件放入容器中
  render: h => h(App),
}).$mount('#app') // 挂载app容器

<!-- 默认index文件-->

<!DOCTYPE html>
<html lang="">
<head>
    <meta charset="utf-8">
    <!--      针对ie浏览器的配置,让ie浏览器以最高级别渲染页面-->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!--      开启移动端的理想视口-->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!--   配置页签图标   -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!--    配置网页标题-->
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- 浏览器不支持js 的时候, noscript中的元素会被渲染-->
<noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
</noscript>
<!-- 容器-->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

render函数

在main.js文件中,
import Vue from 'vue’
,此处导入的vue不是完整的,缺少模板解析器,所以不能使用template属性去设置组件标签

, vue代码完成之后是要打包的, 其中少不了vue的核心代码,当我们打包好之后, 我们其实是不需要再来解析模板的, 那么, vue的核心代码中的模板解析器根本用不着, 所以,vue为了使代码的体积减少, 就去掉了模板解析器,但是我们开发的时候, 又要使用, 所以就创建了一个 render 方法来解析模板,总之, 它的目的就是让打包后的代码体积尽量小,提升性能

关于不同版本的Vue:

  1. vue.js与vue.runtime.xxx.js的区别:
    (1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
    (2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。

  2. 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项

    需要使用render函数接收到的createElement函数去指定具体内容

render
函数主要用于创建虚拟 DOM 节点(VNode)。它是一个 JavaScript 函数,接收一个名为
createElement
(通常缩写为
h
)的函数作为参数。这个
createElement
函数用于构建 VNode

    render(createElement){
        // 创建一个节点  <h1>hello world</h1> 并在页面渲染
        return createElement("h1","hello world") 

    }
// 将App组件传入,将APP中的template模版交给render渲染
// 简写的箭头函数
render: h => h(App),

脚手架默认配置

脚手架默认隐藏了webpack的相关配置,使用下面命令将相关配置输出为一份js文件供查看(仅查看)

vue inspect > output.js

默认情况下,public文件夹下的index、favicon ,src下的App.vue和main.js不允许修改,vue默认是从该路径找对应的文件

可修改的配置
,可以修改脚手架规则,进行个性化定制

ref属性

被用来给元素或子组件注册引用信息(id的替代者)

应用在html标签上获取的是真实DOM元素、组件标签上是组件实例对象(vc)

<template>
  <div id="app">

    <!--   设置标题DOM的ref是title-->
    <h1 ref="title">标题</h1>
    <button @click="showDOm">点击获取标题的DOM元素</button>
    <!--   设置组件ref是studen-->

    <StudentComp ref="student"></StudentComp>
  </div>
</template>

<script>
import StudentComp from "./components/StudentComp.vue";

export default {
  name: 'App',
  components: {
    StudentComp
  },
  data() {
    return {name: "app->vue", address: "Beijing"}
  },
  methods: {
    showDOm() {
      // 这里的this是vc

      //模版标签里ref="title", 在vc上面的结构是 在vc的$refs对象里面有title的key,value就是对应的DOM
      console.log(this.$refs.title)
      // 在组件标签上面使用ref,这里获取到的是对应组件的实例对象
      console.log(this.$refs.student)
    }
  }
}
</script>

props配置

可以让组件接收外部传过来的数据,动态传参

  1. 只接受参数

    <script>
     export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
       // 数组形式->使用props选项,配置city和age参数
      props:["city","age",]
    
    }
    
    
    </script>
    
    <template>
      <div class="student">
        <h1>名字:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        {{city}}
        <!-- 接受到age之后加1,age默认是字符串,所以不会正常计算,只会字符串拼接 --> 
        {{age + 1}} 
    
      </div>
    </template>
    

     <!-- 在使用组件标签的时候,传入对应的数据 -->
    <StudentComp city="beijing" age="18"></StudentComp>
    <!--  如果要传入的是个表达式 ,用数据绑定语法  :age="xxxxx"-->
    
  2. 接收参数-限制类型

    <script>
    export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
    // 对象形式
      props: {
        city: String, // 定义city参数,字符串类型
        age: Number // 定义age参数,Number类型
      }
    }
    
    
    </script>
    
    <template>
      <div class="student">
        <h1>名字:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        {{ city }}
        <!--     age限制是Number类型,可以正常计算-->
        {{ age + 1 }}
    
      </div>
    </template>
    
  3. 接收参数-类型限制-必填限制-默认值设置

    // 对象嵌套对象 
    props:{
        city:{
          type:String, // 字符串类型
          required:true // 必填项,如果不传递,默认是false
        },
        age:{
          type:Number,// number类型
          required: false,// 非必填
          default:18 // 默认值,如果不传参,走默认项
        }
      }
    
  4. props是只读的


    props是只读的,模版标签中传递什么就是什么,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据


    // props的渲染优先级比data高
    // 页面使用myCity进行渲染,myCity的初始值是props的city,从模版标签传入
    // 修改myCity即修改页面效果
    data: function () {
        return {name: "vue", address: "Beijing",myCity:this.city}
      }
    
  5. 通过props传递数据

<template>
  <div>

    <Student  :demo="demo"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  }
}
<script>
 export default {
   // eslint-disable-next-line vue/multi-word-component-names
   name: "Student",
   // 定义props属性
   props:["demo"],
  data: function () {
    return {name: "vue", address: "Beijing"}
  },
  methods: {
    sendStudentName(){
      this.demo(this.name)
    }
  }
}


</script>

mixin混入

可以把多个组件共用的配置提取成一个混入对象

  1. 第一步:定义混合

    // mixin.js文件 
    export const mixin = {
        // 回调
        methods:{
            showName(){
                alert(this.name)
            }
        }
    
    }
    
  2. 第二步:组件配置

    <script>
    
    // 导入mixin
    import {mixin} from "@/mixin";
    
    export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
      // 配置混合(局部混入),导入的mixin
      // 如果有多个 ,数组可以配置多个
      mixins:[mixin]
    
    }
    
    
    </script>
    

    Vue.mixin(mixin) // 全局混入
    
  3. 第三部:正常使用

        <button @click="showName">按钮</button>
    
  4. 一个mixin对象可以报警data、methods、computed等各种组件选项

    理论上可以组件配置所有选项都可以使用,template和el可能会存在一定限制和合并问题

    export const mixin = {
        // 回调
        methods: {
            showName() {
                alert(this.name)
            }
        },
        mounted() {
            console.log("test")
        },
        data() {
            // 如果混合和组件内的data使用了同样的属性,以组件内部为主,如果不相同,两个会组合起来
            return {baseUrl: "xxxx"}
        },
        // 计算
        computed:{
            
        },
        
    }
    

插件

  1. 功能:用于增强Vue

  2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据

  1. 定义插件

    export const TestPlugins  = {
      // 如果需要自定义参数,在Vue后传参即可
        install(Vue){
          // 可以自定义一些方法
            // 过滤器
            Vue.filter()
            // 混入
            Vue.mixin()
    
            // 在Vue原型上添加方法(vm和vc都可以用)
            Vue.property.say = ()=>{}
        }
    }
    
  2. 使用插件

    import {TestPlugins} from "@/plugins";
    
    Vue.use(TestPlugins)
    

scoped样式

多个不同的组件编写的样式,选择器最终是汇总在一起的,有可能出去类名、ID名字冲突,scoped可以让样式在局部生效,防止冲突,原理是在对应的元素上加上一个 data-v-xxxxx,唯一标识

<style scoped ></style>

组件化编码流程

  1. 实现静态组件:抽取组件,使用组件实现静态页面效果
    • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
  2. 展示动态数据:数据类型、名称、保存在哪个组件等等
    • 考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用
    • 一个组件在用:放在组件自身即可
    • 一些组件在用:放在他们共同的父组件上(状态提升)
  3. 交互:绑定事件监听等等
  4. props适用于:
    • 父组件 ==> 子组件 通信
    • 子组件 ==> 父组件 通信(要求父先给子一个函数)

浏览器本地存储

存储储内容大小一般支持5MB左右(不同浏览器可能还不一样)

  1. LocalStorage


    LocalStorage存储的内容,需要手动清除才会消失,关闭浏览器不会消失


        // 本地存储-> 存储到Local Storage
        // key和value都是字符串,若value不是字符串,会默认调toString方法
        // 如果key已经存在,更新数据
        localStorage.setItem("name","vue")
    
        // 读取数据,如果没有该key,读取的是null
        localStorage.getItem("name")
    
        // 删除数据
        localStorage.removeItem("name")
    
        // 清空数据-所有的数据
        localStorage.clear()
    
  2. sessionStorage


    SessionStorage存储的内容会随着浏览器窗口关闭而消失


        // 本地存储-> 存储到Session Storage
         sessionStorage.setItem("name","vue")
    
        // 读取数据,如果没有该key,读取的是null
        sessionStorage.getItem("name")
    
        // 删除数据
        sessionStorage.removeItem("name")
    
    
        // 清空数据-所有的数据
        sessionStorage.clear()
    

组件的自定义事件

在 Vue 中,组件自定义事件是一种用于组件间通信的重要机制。它允许子组件向父组件传递信息。当子组件发生某些操作或者状态变化时,可以通过触发自定义事件来通知父组件

例如,在一个包含表单组件(子组件)和显示数据组件(父组件)的应用中,当用户在表单组件中提交数据后,表单组件可以通过自定义事件将数据传递给父组件,以便父组件进行后续处理

通过自定义事件传递数据
  1. 定义自定义事件-第一种方式
<template>

  <div>
    <!--     给Student这个组件实例对象Vc绑定了一个事件-->
    <!--    给谁绑定了事件,就找谁触发事件-->
    <!--     v-on:自定义事件名="对应方法 "-->
    <!-- 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法 -->    
    <!--  可以简写为@testDemo="demo" -->
    <Student v-on:testDemo="demo"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  }
}
</script>
  1. 定义自定义事件-第二种方式
<template>
  <div>
<!-- 给组件设置ref属性 -->
    <Student  ref="student"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  },
  // 当组件加载完毕之后
  mounted() {
    // student组件实例对象,绑定testDemo事件,触发Demo回调
    // 更加灵活,可以做一些前后置操作再绑定事件
    // 该方式回调要么配置在methods中,要么用箭头函数,写普通函数中的this是要绑定的组件实例对象
    this.$refs.student.$on("testDemo",this.demo)
  }
}
</script>
  1. 触发自定义事件
<script>
 export default {
   name: "Student",
  data: function () {
    return {name: "vue", address: "Beijing"}
  },
  methods: {
    sendStudentName(){
      //  通过$emit方法触发事件,传入自定义事件的名字
      //  触发testDemo事件
      this.$emit("testDemo",this.name)
    }
  }
}


</script>

<template>
   <div class="student">
    <h1>名字:{{ name }}</h1>
    <h1>地址:{{ address }}</h1>
    <button @click="sendStudentName">按钮</button>

  </div>


</template>
解绑自定义事件
// 在要解绑的组件上调用解绑指定组件   
this.$off("testDemo")
// 解绑多个指定事件
this.$off(["testDemo","xxxx"])
// 所有的自定义事件都解绑
this.$off()
native

如果在组件标签上绑定原生的事件,组件标签也会当成自定义事件去寻找自定义的方法

<!-- 使用native修饰符,告诉组件这个是原生事件 -->
<Student  ref="student" @click.native="show"></Student>

全局事件总线

一种组件间通信的方式,用于任意组件间通信,在应用程序中实现事件通信的机制,它允许不同的组件之间进行解耦的通信。可以把它想象成一个消息中心,各个组件或模块可以向这个中心发布(触发)事件,也可以从这个中心订阅(监听)事件。当一个事件被发布时,所有订阅了该事件的组件或模块都会收到通知并可以执行相应的操作

创建全局事件总线

VueComponent.prototype.
__proto__
=== Vue.prototype

$on、$off、$emit都是Vue原型上的方法,组件实例对象(vc)可以访问到Vue原型上的属性、方法

可以通过创建一个新的 Vue 实例来作为全局事件总线。通常在项目的入口文件中进行设置

// 入口文件
new Vue({
    render: h => h(App),
    // 生命周期函数:beforeCreate 组件实例刚被创建时调用
    beforeCreate() {

        // 在vue的原型上添加一个$bus属性,值是当前vue实例 

         Vue.prototype.$bus = this
    }
}).$mount('#app')
使用全局事件总线

接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身

// 子组件A
// 组件挂载后绑定事件
mounted() {
    // 在bus(vue)上绑定一个test事件
    this.$bus.$on("test",(data)=>{
      console.log("收到数据",data)
    })
  },
 // 组件销毁之前解绑
    beforeDestroy(){
      this.$bus.$off("test")
    }
    
触发全局事件总线
// 子组件B
sendData(){
      // 触发bus(vue)的test事件
      this.$bus.$emit("test","data数据")

    }

消息订阅与发布

PubSub 的概念
  • PubSub 是 Publish - Subscribe(发布 - 订阅)的缩写,它本质上是一种消息传递模式。在这种模式下,有发布者(Publishers)和订阅者(Subscribers)两类角色。发布者负责产生消息并将其发送到一个消息中心(也称为消息代理,Message Broker),订阅者则向消息中心表达自己对某些消息类型的兴趣,当消息中心收到发布者发送的匹配订阅者兴趣的消息时,就会将消息转发给订阅者

  • 一种组件间通信的方式,适用于任意组件间通信

  • 有一些专门的 PubSub 库可以帮助实现消息的发布和订阅。例如pubsu-js库

使用pubsub
  1. 安装pubsu-js库

    npm install -g pubsub-js       
    
  2. 引入pubsub

    import pubsub from 'pubsub-js'
    
  3. 订阅消息


    A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身


    // 组件挂载完之后订阅消息
    mounted() {
        // 参数:主题、回调函数(主题名、数据)
        // 订阅了testTopic主题的消息,如果有人给testTopic主题发消息,会接受到
        // pubsub.subscribe会返回一个订阅id
        this.pid = pubsub.subscribe("testTopic",function (msgName,data){
          // 此时this是undefined,因为使用的三方库
          // 可以使用箭头函数,this是当前vc 或者配置回调函数在这里调用
          console.log(data)
    
        })
      },
        // 组件销毁前
      beforeDestroy(){
        // 取消订阅(订阅id)
        pubsub.unsubscribe(this.pid)
      }
    
  4. 发布消息

        // 给订阅testTopic主题的发送消息,发送消息内容是helloworld
        pubsub.publish("testTopic","helloworld")
    

$nextTick

$nextTick
是 Vue.js 提供的一个实例方法,它的主要作用是在下次 DOM 更新循环结束之后执行延迟回调。在 Vue 中,数据的变化到 DOM 的更新是异步的,当数据发生改变时,Vue 会开启一个异步更新队列,将同一事件循环中的所有数据变化引起的 DOM 更新操作合并到一个更新任务中,等本轮事件循环结束后,再一次性执行 DOM 更新。
$nextTick
就是用于在这个 DOM 更新完成后执行一些操作

     edit(todo){
       if(todo.hasOwnProperty('isEdit')){
        todo.isEdit = true
      }else {
        this.$set(todo,'isEdit',true)
      }
       this.$refs.inputTitle.focus()
     }
// 问题示例:我们想修改一个状态,状态是true的时候输入框可以获取获取焦点
// 问题在于,todo.isEdit = true修改后,vue不是立马渲染的,而是等代码都执行完才渲染
// 相当于this.$refs.inputTitle.focus() 执行完 再去渲染,
// 执行的时候页面还没有渲染,是false,从而获取焦点失败
// 当页面渲染完后,再去获取焦点
this.$nextTick(function (){
        this.$refs.inputTitle.focus()
      })
  1. 作用:在下一次 DOM 更新结束后执行其指定的回调。

  2. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行

过渡与动画

在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名

image-20241121135250739

动画默认名称效果

vue会根据规则自动实现动画效果,适用一个动画

<template>
  <div>
    <button @click="isShow = !isShow"> 显示/隐藏</button>
    <!--   让谁有动画效果,就用transition 把谁包起来 -->
    <!--   appear属性用于控制元素在初始渲染时是否应用过渡效果 不加则初始渲染不应用-->
    <!-- 只适用于包裹单个元素 -->
    <transition appear>
      <h1 v-show="isShow">hello world</h1>

    </transition>

  </div>
</template>

<style scoped>
h1 {
  background-color: orange;
}

/* v-enter-active主要用于定义元素进入(插入)时过渡动画的行为 */
.v-enter-active {
  animation: test 1s;
}

/* v-leave-active用于定义元素离开(移除)时过渡动画的行为 */
.v-leave-active {
  animation: test 1s reverse;
}

/* 定义动画关键帧 */
@keyframes test {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0px);
  }

}
</style>
动画自定义名称

设置自定义名称,可以指定多个不同的效果

<!-- 指定name名称-->
<transition appear name="hello">
      <h1 v-show="isShow">hello world</h1>
 
    </transition>
/* 使用指定name */
.hello-enter-active {
  animation: test 1s;
}

/* 使用指定name */
.hello-leave-active {
  animation: test 1s reverse;
}
过渡效果
<template>
  <div>
    <button @click="isShow = !isShow"> 显示/隐藏</button>
    <!--   让谁有动画效果,就用transition 把谁包起来 -->
    <!--   appear属性用于控制元素在初始渲染时是否应用过渡效果 不加则初始渲染不应用-->
    <transition appear>
      <h1 v-show="isShow">hello world</h1>

    </transition>

  </div>
</template>

<style scoped>

h1{
  background-color: orange;
  transition: 0.5s linear;
}
/* 进入的起点 */
.hello-enter {
  transform: translateX(-100%);
}

/* 进入的终点 */
.hello-enter-to {
  transform: translateX(0);

}

/* 离开的起点 */
.hello-leave{
  transform: translateX(0%);

}

/* 离开的终点  */
.hello-leave-to{
  transform: translateX(-100%);

}
</style>

合并写法

h1{
  background-color: orange;
}

/* 进入的起点和离开的终点 */
.hello-enter,.hello-leave-to {
  transform: translateX(-100%);
}

/* 不去修改h1本身的样式,使用进入和离开的时候设置效果 */
.hello-enter-active,.hello-leave-active {
  transition: 0.5s linear;
}

/* 进入的终点和离开的启动 */
.hello-enter-to,.hello-leave {
  transform: translateX(0);

}
多个元素过渡
<!--  transition只能包裹一个元素   -->
<!--   包裹多个元素使用transition-group,且每个元素都有一个key值 -->
    <transition-group appear name="hello">
      <h1 v-show="isShow" key="1">hello world</h1>
      <h1 v-show="isShow" key="2">hello vue</h1>
      <h1 v-show="isShow" key="3">hello html</h1>

    </transition-group>
集成第三方动画

可以使用现成的三方动画库来实现效果

animate动画库文档(墙)

  1. 安装

    npm install animate.css --save
    
  2. 引入

    import "animate.css"
    
    
  3. 配置

        <!-- 配置name-->
        <!--   配置进入的效果  在该库文档上选用对应效果的名字-->
        <!--   配置离开的效果 在该库文档上选用对应效果的名字-->
    
        <transition-group
            appear
            name="animate__animated animate__bounce"
            enter-active-class="animate__bounceOutRight"
            leave-active-class="animate__fadeInDown"
        >
          <h1 v-show="isShow" key="1">hello world</h1>
          <h1 v-show="isShow" key="2">hello vue</h1>
          <h1 v-show="isShow" key="3">hello html</h1>
    
        </transition-group>
    

前言

在工业和科研领域,环境监测系统的重要性日益凸显。上位机软件作为环境监测系统的关键组成部分,负责数据采集、处理和显示,对提高监测效率和准确性起着至关重要的作用。

本文将向大家介绍一款用 C# 开发的环境监测上位机软件。

软件介绍

本上位机软件为广西北海渔场监控项目定制的监控上位机软件。

开发环境

开发工具:Visual Studio 2019

开发框架:WinForms

使用控件:BeauGaugeInstrumentationSuitePro

主要语言:C#

主要功能

1、使用环境

  • 在数据资料目录下先安装以下控件和软件
  • 安装 BeauGaugeInstrumentationSuitePro 控件:
  • 运行BeauGaugeInstrumentationSuitePro.msi 安装包进行安装。
  • 安装上位机软件
  • 运行北部湾深海网箱养殖水质环境监控系统.msi 安装包进行安装。
  • 或者,您可以下载绿色版压缩文件,解压后直接双击运行recvdatasection.exe文件。

2、界面介绍

本上位机软件分为3个从监测节点和1个主监测节点:

  • 从节点:温度、盐度、浊度、PH值、溶解氧
  • 主节点:气温、风速、气压、俯仰角、横滚角、雨量、经纬度

同时,软件还隐藏了3个备用从节点,以备不时之需。

3、重启

软件菜单重启按钮:重新内部启动上位机软件

4. 设备管理

可以进行各种设备的管理。

点击打开和关闭则对应的相关设备进行通电和断电操作。

5、数据查看

对应监测点数据曲线图

每个监测点的数据曲线如下为例:

历史曲线图需点击如下图箭头所示按钮,弹出当天的数据信息如下所示:

6、历史
数据

可以进行各种历史数据操作,可以选择历史时间点并打开该天的数据图。

7、低信号模式

当下位机数据传输不稳定或者节点开启但无数据显示可打开低信号模式。

8、阈值设置

点击阈值设置按钮可展出阈值设置界面,再次点击隐藏,如下图:

初始化阈值设定:点击初始化阈值设定按钮打开初始阈值界面,可设定传感器数据监测范围,设定完后保存。点取消退出设定界面。初始阈值数据保存本地,即软件刚使用时设定完即可,一般不可更改。

重置:重置按钮即将初始化设定的阈值数据全部覆盖当前阈值范围数据,并关闭监测的作用。

一键开启:打开全部传感器监测数据。可手动单独开启,开启按钮提示为蓝色,关闭为白色。

若监测点数据异常在消息界面以黄色警告字体提示,同时阈值显示界面异常数据文本变红色,且每分钟自动播放2秒提示音。

项目地址

Gitee:
https://gitee.com/tytokongjian /beihai-host-computer

总结

此为北海项目中的一部分。监控上位机模板项目,应用于实际生产环境,需要下位机传感器配合使用。

希望通过本文能为大家在上位机软件开发方面提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

inference-by-vllm.png

本文主要分享如何使用 vLLM 实现大模型推理服务。

1. 概述

大模型推理有多种方式比如

  • 最基础的 HuggingFace Transformers
  • TGI
  • vLLM
  • Triton + TensorRT-LLM
  • ...

其中,热度最高的应该就是 vLLM,性能好的同时使用也非常简单,本文就分享一下如何使用 vLLM 来启动大模型推理服务。

根据 vLLM 官方博客
vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
所说:

进行了 NVIDIA A10 GPU 上推理 LLaMA-7 B 和 在 NVIDIA A100 GPU(40 GB)上推理 LLaMA-13 B 两个实验,
在吞吐量上 vLLM 比最基础的 HuggingFace Transformers 高 24 倍,比 TGI 高 3.5 倍

vllm-performance.png

2.安装 vLLM

首先要准备一个 GPU 环境,可以参考这篇文章:
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU

需要保证宿主机上可以正常执行
nvidia-smi
命令,就像这样:

root@test:~# nvidia-smi
Thu Jul 18 10:52:01 2024
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.147.05   Driver Version: 525.147.05   CUDA Version: 12.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  NVIDIA A40          Off  | 00000000:00:07.0 Off |                    0 |
|  0%   45C    P0    88W / 300W |  40920MiB / 46068MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
|   1  NVIDIA A40          Off  | 00000000:00:08.0 Off |                    0 |
|  0%   47C    P0    92W / 300W |  40916MiB / 46068MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|    0   N/A  N/A   1847616      C   tritonserver                      480MiB |
|    0   N/A  N/A   2553571      C   python3                         40426MiB |
|    1   N/A  N/A   1847616      C   tritonserver                      476MiB |
|    1   N/A  N/A   2109313      C   python3                         40426MiB |
+-----------------------------------------------------------------------------+

安装 conda

为了避免干扰,这里使用 conda 单独创建一个 Python 虚拟环境安装 vLLM。

使用下面的命令可以快速安装最新的 miniconda,也可以去官方下载压缩包,解压并配置到 PATH 变量。

mkdir -p ~/miniconda3

wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh

bash ~/miniconda3/miniconda.sh -b -u -p ~/miniconda3

rm -rf ~/miniconda3/miniconda.sh

然后初始化

# 根据使用的不同 shell 选择一个命令执行
~/miniconda3/bin/conda init bash
~/miniconda3/bin/conda init zsh

激活

source ~/.bashrc

创建虚拟环境安装 vLLM

创建虚拟环境并激活

conda create -n vllm_py310 python=3.10

conda activate vllm_py310

# 配置 pip 源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/
pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn

# 在虚拟环境中安装 vllm 0.4.2 版本
pip install vllm==0.4.2

3. 准备模型

一般模型都会发布到
HuggingFace
,不过国内网络情况,推荐到
ModelScope
下载。

主流的模型 vLLM 都是支持的,具体列表可以查看官方文档:
vllm-supported-models

这里使用 Qwen1.5-1.8B-Chat 进行测试。

使用 git lfs 方式下载:

# 安装并初始化 git-lfs
apt install git-lfs -y
git lfs install

# 下载模型
git lfs clone https://www.modelscope.cn/qwen/Qwen1.5-1.8B-Chat.git

完整内容如下:

root@j99cloudvm:~/lixd/models# ls -lhS Qwen1.5-1.8B-Chat/
total 3.5G
-rw-r--r-- 1 root root 3.5G Jul 18 11:20 model.safetensors
-rw-r--r-- 1 root root 6.8M Jul 18 11:08 tokenizer.json
-rw-r--r-- 1 root root 2.7M Jul 18 11:08 vocab.json
-rw-r--r-- 1 root root 1.6M Jul 18 11:08 merges.txt
-rw-r--r-- 1 root root 7.2K Jul 18 11:08 LICENSE
-rw-r--r-- 1 root root 4.2K Jul 18 11:08 README.md
-rw-r--r-- 1 root root 1.3K Jul 18 11:08 tokenizer_config.json
-rw-r--r-- 1 root root  662 Jul 18 11:08 config.json
-rw-r--r-- 1 root root  206 Jul 18 11:08 generation_config.json
-rw-r--r-- 1 root root   51 Jul 18 11:08 configuration.json

这个目录包含了一个大模型的相关文件。以下是每个文件的作用简要说明:

  • model.safetensors
    :这是大模型的主要文件,包含了模型的权重。
  • tokenizer.json
    :这个文件包含了分词器(Tokenizer)的配置和词汇表。分词器用于将输入文本转换为模型可以处理的格式,通常是数字 ID。
  • tokenizer_config.json
    :这个文件包含了分词器的配置选项,如分词器的类型、参数设置等。
  • config.json
    :这个文件包含了模型的配置参数,定义了模型的结构和训练过程中的一些设置。它通常包括层数、隐藏单元数、激活函数等参数。
  • generation_config.json
    :这个文件包含了用于生成文本的配置参数,如生成长度、采样策略等。
  • configuration.json
    :这个文件通常是模型的额外配置文件,可能包含与模型结构或训练过程相关的配置信息。
  • vocab.json
    :这个文件包含了分词器的词汇表,通常是一个从词汇到 ID 的映射表。分词器使用这个文件来将文本中的单词转换为模型可以处理的 ID。

一般只需要注意权重文件和 tokenizer 即可。

4. 开始推理

启动推理服务

vLLM 支持提供 OpenAI 格式的 API,启动命令如下:

modelpath=/models/Qwen1.5-1.8B-Chat

# 单卡
python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --trust-remote-code

输出如下

INFO 07-18 06:42:31 llm_engine.py:100] Initializing an LLM engine (v0.4.2) with config: model='/models/Qwen1.5-1.8B-Chat', speculative_config=None, tokenizer='/models/Qwen1.5-1.8B-Chat', skip_tokenizer_init=False, tokenizer_mode=auto, revision=None, tokenizer_revision=None, trust_remote_code=True, dtype=torch.float16, max_seq_len=32768, download_dir=None, load_format=LoadFormat.AUTO, tensor_parallel_size=1, disable_custom_all_reduce=False, quantization=None, enforce_eager=False, kv_cache_dtype=auto, quantization_param_path=None, device_config=cuda, decoding_config=DecodingConfig(guided_decoding_backend='outlines'), seed=0, served_model_name=qwen)
INFO:     Started server process [614]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

vLLM 默认监听 8000 端口。

对于多卡则是增加参数
tensor-parallel-size
,将该参数设置为 GPU 数量即可,vLLM 会启动 ray cluster 将模型切分到多个 GPU 上运行,对于大模型很有用。

python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --tensor-parallel-size 8 \
        --trust-remote-code

发送测试请求

直接使用 OpenAI 格式请求

# model 就是前面启动服务时的 served-model-name 参数
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "qwen",
        "messages": [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "你是谁?"}
        ]
    }'

输出如下:

{"id":"cmpl-07f2f8c70bd44c10bba71d730e6e10a3","object":"chat.completion","created":1721284973,"model":"qwen","choices":[{"index":0,"message":{"role":"assistant","content":"我是来自阿里云的大规模语言模型,我叫通义千问。我是阿里云自主研发的超大规模语言模型,可以回答问题、创作文字,还能表达观点、撰写代码、撰写故事,还能表达观点、撰写代码、撰写故事。我被设计用来帮助用户解答问题、创作文字、表达观点、撰写代码、撰写故事,以及进行其他各种自然语言处理任务。如果您有任何问题或需要帮助,请随时告诉我,我会尽力提供支持。"},"logprobs":null,"finish_reason":"stop","stop_reason":null}],"usage":{"prompt_tokens":22,"total_tokens":121,"completion_tokens":99}}

查看 GPU 占用情况,基本跑满了

Thu Jul 18 06:45:32 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.161.08             Driver Version: 535.161.08   CUDA Version: 12.2     |
|-----------------------------------------+----------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                         |                      |               MIG M. |
|=========================================+======================+======================|
|   0  Tesla T4                       On  | 00000000:3B:00.0 Off |                    0 |
| N/A   59C    P0              69W /  70W |  12833MiB / 15360MiB |     84%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+
|   1  Tesla T4                       On  | 00000000:86:00.0 Off |                    0 |
| N/A   51C    P0              30W /  70W |   4849MiB / 15360MiB |      0%      Default |
|                                         |                      |                  N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|    0   N/A  N/A   3376627      C   python3                                   12818MiB |
|    1   N/A  N/A   1150487      C   /usr/bin/python3                           4846MiB |
+---------------------------------------------------------------------------------------+

5. 小结

本文主要分享如何使用 vLLM 来部署大模型推理服务, 安装好环境后,vLLM 使用非常简单,一条命令即可启动。

modelpath=/models/Qwen1.5-1.8B-Chat

# 单卡
python3 -m vllm.entrypoints.openai.api_server \
        --model $modelpath \
        --served-model-name qwen \
        --trust-remote-code


【Kubernetes 系列】
持续更新中,搜索公众号【
探索云原生
】订阅,阅读更多文章。


Torus
类在制作数学、物理或工程领域的动画时具有广泛的应用场景。

比如,通过动态演示环面的拓扑变换(如内外翻转、扭曲等),帮助我们直观地理解拓扑不变量和同胚等概念;

此外,也可以模拟磁场线在环面导体中的分布和运动,展示电磁感应现象等等。

本篇介绍Torus的主要参数和基本使用方法。

1. 主要参数

Torus
的参数不多,主要有:

参数名称 类型 说明
major_radius float 圆环面的主要半径,从环面中心到其管道中心(或称为环面中心轴)的距离
minor_radius float 环面管道的半径
resolution [int, int] 环面表面的分辨率,用于控制渲染的精细程度
u_range [float] 定义了圆环面在u方向上的参数化范围
v_range [float] 定义了圆环面在v方向上的参数化范围

如果把
Torus
圆环面看成一个轮胎的话,

那么,
major_radius
参数表示轮胎的大小,
minor_radius
参数表示轮胎的厚度。

2. 使用示例

为了有效展示
Torus
(圆环面)各个参数的使用,下面构造四个示例,

每个示例将突出
Torus
类的一个或几个关键参数,并说明这些参数如何影响环面的外观和特性。

2.1. 标准圆环面

这是一个标准的环面,其中
major_radius
决定了环面的大小,
minor_radius
决定了环面管道的厚度,


resolution
控制了环面表面的平滑度。

通过调整这些参数,可以获得不同大小和形状的环面。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
)

2.2. 扁平圆环面

通过限制
v_range
的值,我们可以创建一个扁平的环面。

在这个示例中,
v_range
被设置为
(0, PI/2)
,这意味着环面在
v方向
上的参数化范围被限制在一个更小的区间内,从而导致环面在视觉上变得更加扁平。

这种扁平环面可以用于模拟轮胎、甜甜圈等扁平形状的物体。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
    v_range=(0, PI / 2),
)

2.3. 高分辨率圆环面

增加
resolution
的值可以提高环面表面的平滑度和细节程度。

在这个示例中,
resolution
被设置为
(100, 100)
,这意味着环面在
u和v方向
上都有更高的分辨率,从而呈现出更加细腻和逼真的曲面效果。

高分辨率环面在渲染复杂场景或制作高质量动画时非常有用。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(100, 100),
)

2.4. 非标准圆环面

通过调整
u_range
的值,我们可以创建一个非标准的环面。

在这个示例中,
u_range
被设置为
(0, 3*PI/2)
,这意味着环面在
u方向
上的参数化范围被扩展到一个更大的区间内,从而导致环面在视觉上出现一部分缺失。

这种非标准环面可以用于艺术创作、数学可视化或物理模拟等领域,以展示环面在不同参数设置下的多样性和灵活性。

torus = Torus(
    major_radius=2.5,
    minor_radius=0.5,
    resolution=(30, 30),
    u_range=(0, 3 * PI / 2),
)

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
torus.py
),

下载地址:
完整代码
(访问密码: 6872)


最近在复现 PPO 跑 MiniGrid,记录一下…

这里跑的环境是 Empty-5x5 和 8x8,都是简单环境,主要验证 PPO 实现是否正确。

01 Proximal policy Optimization(PPO)

(参考:
知乎 | Proximal Policy Optimization (PPO) 算法理解:从策略梯度开始

首先,
策略梯度方法
的梯度形式是

\[\nabla_\theta J(\theta)\approx
\frac1n \sum_{i=0}^{n-1} R(\tau_i)
\sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(a_t|s_t)
\tag1
\]

然而,传统策略梯度方法容易一步走的太多,以至于越过了中间比较好的点(在参考知乎博客里称为 overshooting)。一个直观的想法是限制策略每次不要更新太多,比如去约束 新策略 旧策略之间的 KL 散度(公式是 plog(p/q)):

\[D_{KL}(\pi_\theta | \pi_{\theta+\Delta \theta}) = \mathbb E_{s,a}
\pi_\theta(a|s)\log\frac{\pi_\theta(a|s)}{\pi_{\theta+\Delta \theta}(a|s)} \le \epsilon
\tag2
\]

我们把这个约束进行拉格朗日松弛,将它变成一个惩罚项:

\[\Delta\theta^* = \arg\max_{\Delta\theta} J(\theta+\Delta\theta) -
\lambda [D_{KL}(\pi_\theta | \pi_{\theta+\Delta \theta})-\epsilon]
\tag3
\]

然后再使用一些数学近似技巧,可以得到自然策略梯度(NPG)算法。

NPG 算法貌似还有种种问题,比如 KL 散度的约束太紧,导致每次更新后的策略性能没有提升。我们希望每次策略更新后都带来性能提升,因此计算 新策略 旧策略之间 预期回报的差异。这里采用计算 advantage 的方式:

\[J(\pi_{\theta+\Delta\theta})=J(\pi_{\theta})+\mathbb E_{\tau\sim\pi_{\theta+\Delta\theta}}\sum_{t=0}^\infty
\gamma^tA^{\pi_{\theta}}(s_t,a_t)
\tag{4}
\]

其中优势函数(advantage)的定义是:

\[A^{\pi_{\theta}}(s_t,a_t)=\mathbb E(Q^{\pi_{\theta}}(s_t,a_t)-V^{\pi_{\theta}}(s_t))
\tag{5}
\]

在公式 (4) 中,我们计算的 advantage 是在 新策略 的期望下的。但是,在新策略下蒙特卡洛采样(rollout)来算 advantage 期望太麻烦了,因此我们在原策略下 rollout,并进行 importance sampling,假装计算的是新策略下的 advantage。这个 advantage 被称为替代优势(surrogate advantage):

\[\mathcal{L}_{\pi_{\theta}}\left(\pi_{\theta+\Delta\theta}\right) =
J\left(\pi_{\theta+\Delta\theta}\right)-J\left(\pi_{\theta}\right)\approx E_{s\sim\rho_{\pi\theta}}\frac{\pi_{\theta+\Delta\theta}(a\mid s)}{\pi_{\theta}(a\mid s)} A^{\pi_{\theta}}(s, a)
\tag6
\]

所产生的近似误差,貌似可以用两种策略之间最坏情况的 KL 散度表示:

\[J(\pi_{\theta+\Delta\theta})-J(\pi_{\theta})\geq\mathcal{L}_{\pi\theta}(\pi_{\theta+\Delta\theta})-CD_{KL}^{\max}(\pi_{\theta}||\pi_{\theta+\Delta\theta})
\tag7
\]

其中 C 是一个常数。这貌似就是 TRPO 的单调改进定理,即,如果我们改进下限 RHS,我们也会将目标 LHS 改进至少相同的量。

基于 TRPO 算法,我们可以得到 PPO 算法。PPO Penalty 跟 TRPO 比较相近:

\[\Delta\theta^{*}=\underset{\Delta\theta}{\text{argmax}}
\Big[\mathcal{L}_{\theta+\Delta\theta}(\theta+\Delta\theta)-\beta\cdot \mathcal{D}_{KL}(\pi_{\theta}\parallel\pi_{\theta+\Delta\theta})\Big]
\tag 8
\]

其中,KL 散度惩罚的 β 是启发式确定的:PPO 会设置一个目标散度
\(\delta\)
,如果最终更新的散度超过目标散度的 1.5 倍,则下一次迭代我们将加倍 β 来加重惩罚。相反,如果更新太小,我们将 β 减半,从而扩大信任域。

接下来是 PPO Clip,这貌似是目前最常用的 PPO。PPO Penalty 用 β 来惩罚策略变化,而 PPO Clip 与此不同,直接限制策略可以改变的范围。我们重新定义 surrogate advantage:

\[\begin{aligned}
\mathcal{L}_{\pi_{\theta}}^{CLIP}(\pi_{\theta_{k}}) = \mathbb E_{\tau\sim\pi_{\theta}}\bigg[\sum_{t=0}^{T}
\min\Big( & \rho_{t}(\pi_{\theta}, \pi_{\theta_{k}})A_{t}^{\pi_{\theta_{k}}},
\\
& \text{clip} (\rho_{t}(\pi_{\theta},\pi_{\theta_{k}}), 1-\epsilon, 1+\epsilon) A_{t}^{\pi_{\theta_{k}}}
\Big)\bigg]
\end{aligned}
\tag 9
\]

其中,
\(\rho_{t}\)
为重要性采样的 ratio:

\[\rho_{t}(\theta)=\frac{\pi_{\theta}(a_{t}\mid s_{t})}{\pi_{\theta_{k}}(a_{t}\mid s_{t})}
\tag{10}
\]

公式 (9) 中,min 括号里的第一项是 ratio 和 advantage 相乘,代表新策略下的 advantage;min 括号里的第二项是对 ration 进行的 clip 与 advantage 的相乘。这个 min 貌似可以限制策略变化不要太大。

02 如何复现 PPO(参考 stable baselines3 和 clean RL)

代码主要结构如下,以 stable baselines3 为例:(仅保留主要结构,相当于伪代码,不保证正确性)

import torch
import torch.nn.functional as F
import numpy as np

# 1. collect rollout
self.policy.eval()
rollout_buffer.reset()
while not done:
    actions, values, log_probs = self.policy(self._last_obs)
    new_obs, rewards, dones, infos = env.step(clipped_actions)
    rollout_buffer.add(
        self._last_obs, actions, rewards,
        self._last_episode_starts, values, log_probs,
    )
    self._last_obs = new_obs
    self._last_episode_starts = dones

with torch.no_grad():
    # Compute value for the last timestep
    values = self.policy.predict_values(obs_as_tensor(new_obs, self.device)) 

rollout_buffer.compute_returns_and_advantage(last_values=values, dones=dones)


# 2. policy optimization
for rollout_data in self.rollout_buffer.get(self.batch_size):
    actions = rollout_data.actions
    values, log_prob, entropy = self.policy.evaluate_actions(rollout_data.observations, actions)
    advantages = rollout_data.advantages
    # Normalize advantage
    if self.normalize_advantage and len(advantages) > 1:
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    # ratio between old and new policy, should be one at the first iteration
    ratio = torch.exp(log_prob - rollout_data.old_log_prob)

    # clipped surrogate loss
    policy_loss_1 = advantages * ratio
    policy_loss_2 = advantages * torch.clamp(ratio, 1 - clip_range, 1 + clip_range)
    policy_loss = -torch.min(policy_loss_1, policy_loss_2).mean()

    # Value loss using the TD(gae_lambda) target
    value_loss = F.mse_loss(rollout_data.returns, values_pred)

    # Entropy loss favor exploration
    entropy_loss = -torch.mean(entropy)

    loss = policy_loss + self.ent_coef * entropy_loss + self.vf_coef * value_loss

    # Optimization step
    self.policy.optimizer.zero_grad()
    loss.backward()
    # Clip grad norm
    torch.nn.utils.clip_grad_norm_(self.policy.parameters(), self.max_grad_norm)
    self.policy.optimizer.step()

大致流程:收集当前策略的 rollout → 计算 advantage → 策略优化。

计算 advantage 是由 rollout_buffer.compute_returns_and_advantage 函数实现的:

rb = rollout_buffer
last_gae_lam = 0
for step in reversed(range(buffer_size)):
    if step == buffer_size - 1:
        next_non_terminal = 1.0 - dones.astype(np.float32)
        next_values = last_values
    else:
        next_non_terminal = 1.0 - rb.episode_starts[step + 1]
        next_values = rb.values[step + 1]
    delta = rb.rewards[step] + gamma * next_values * next_non_terminal - rb.values[step]  # (1)
    last_gae_lam = delta + gamma * gae_lambda * next_non_terminal * last_gae_lam  # (2)
    rb.advantages[step] = last_gae_lam
rb.returns = rb.advantages + rb.values

其中,

  • (1) 行通过类似于 TD error 的形式(A = r + γV(s') - V(s)),计算当前 t 时刻的 advantage;
  • (2) 行则是把 t+1 时刻的 advantage 乘 gamma 和 gae_lambda 传递过来。

03 记录一些踩坑经历

  1. PPO 在收集 rollout 的时候,要在分布里采样,而非采用 argmax 动作,否则没有 exploration。(PPO 在分布里采样 action,这样来保证探索,而非使用 epsilon greedy 等机制;听说 epsilon greedy 机制是 value-based 方法用的)
  2. 如果 policy 网络里有(比如说)batch norm,rollout 时应该把 policy 开 eval 模式,这样就不会出错。
  3. (但是,不要加 batch norm,加 batch norm 性能就不好了。听说 RL 不能加 batch norm)
  4. minigrid 简单环境,RNN 加不加貌似都可以(?)
  5. 在算 entropy loss 的时候,要用真 entropy,从 Categorical 分布里得到的 entropy;不要用 -logprob 近似的,不然会导致策略分布 熵变得很小 炸掉。