分类 其它 下的文章

【引言】

本项目是一个简单的随机数生成器应用,用户可以通过设置随机数的范围和个数,并选择是否允许生成重复的随机数,来生成所需的随机数列表。生成的结果可以通过点击“复制”按钮复制到剪贴板。

【环境准备】

• 操作系统:Windows 10
• 开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
• 目标设备:华为Mate60 Pro
• 开发语言:ArkTS
• 框架:ArkUI
• API版本:API 12

【关键技术点】

1. 用户界面设计

用户界面主要包括以下几个部分:

• 标题栏:显示应用名称。
• 输入框:用户可以输入随机数的起始值、结束值和生成个数。
• 开关:用户可以选择生成的随机数是否允许重复。
• 生成按钮:点击后生成随机数。
• 结果显示区:显示生成的随机数,并提供复制功能。

2. 随机数生成算法

随机数生成是本项目的重点。根据用户是否允许生成重复的随机数,算法分为两种情况:

2.1 不允许重复

当用户选择不允许生成重复的随机数时,程序使用一个 Set 来存储生成的随机数,利用 Set 的特性自动去重。具体步骤如下:

1)计算范围:计算用户指定的随机数范围 range = endValue - startValue + 1。

2)生成随机数:使用一个临时数组 tempArray 来辅助生成不重复的随机数。每次生成一个随机索引 randomIndex,从 tempArray 中取出或计算一个新的随机数 randomNum,并将其添加到 Set 中。

3)更新临时数组:将 tempArray 中末尾的元素移动到随机位置,以确保下次生成的随机数仍然是唯一的。

if (!this.isUnique) {
  if (countValue > range) {
    // 显示错误提示
    this.getUIContext().showAlertDialog({
      title: '错误提示',
      message: `请求的随机数数量超过了范围内的总数`,
      confirm: {
        defaultFocus: true,
        value: '我知道了',
        fontColor: Color.White,
        backgroundColor: this.primaryColor,
        action: () => {}
      },
      onWillDismiss: () => {},
      alignment: DialogAlignment.Center,
    });
    return;
  }
  for (let i = 0; i < countValue; i++) {
    let randomIndex = Math.floor(Math.random() * (range - i));
    let randomNum = 0;
    if (tempArray[randomIndex] !== undefined) {
      randomNum = tempArray[randomIndex];
    } else {
      randomNum = startValue + randomIndex;
    }
    generatedNumbers.add(randomNum);
    if (tempArray[range - 1 - i] === undefined) {
      tempArray[range - 1 - i] = startValue + range - 1 - i;
    }
    tempArray[randomIndex] = tempArray[range - 1 - i];
  }
  this.generatedNumbers = JSON.stringify(Array.from(generatedNumbers));
}

3. 剪贴板功能

为了方便用户使用,程序提供了将生成的随机数复制到剪贴板的功能。具体实现如下:

private copyToClipboard(text: string): void {
  const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
  const systemPasteboard = pasteboard.getSystemPasteboard();
  systemPasteboard.setData(pasteboardData);
  promptAction.showToast({ message: '已复制' });
}

【完整代码】

// 导入剪贴板服务模块,用于后续实现复制功能
import { pasteboard } from '@kit.BasicServicesKit';
// 导入用于显示提示信息的服务
import { promptAction } from '@kit.ArkUI';

// 使用装饰器定义一个入口组件,这是应用的主界面
@Entry
@Component
struct RandomNumberGenerator {
  // 定义基础间距,用于布局中的间距设置
  @State private baseSpacing: number = 30;
  // 存储生成的随机数字符串
  @State private generatedNumbers: string = '';
  // 应用的主题色
  @State private primaryColor: string = '#fea024';
  // 文本的颜色
  @State private fontColor: string = "#2e2e2e";
  // 输入框是否获取焦点的状态变量
  @State private isFocusStart: boolean = false;
  @State private isFocusEnd: boolean = false;
  @State private isFocusCount: boolean = false;
  // 是否允许生成的随机数重复
  @State private isUnique: boolean = true;
  // 随机数生成的起始值
  @State private startValue: number = 0;
  // 随机数生成的结束值
  @State private endValue: number = 0;
  // 要生成的随机数个数
  @State private countValue: number = 0;

  // 生成随机数的方法
  private generateRandomNumbers(): void {
    const startValue = this.startValue; // 获取当前设定的起始值
    const endValue = this.endValue; // 获取当前设定的结束值
    const countValue = this.countValue; // 获取当前设定的生成个数
    const range: number = endValue - startValue + 1; // 计算生成范围


    // 用于存储生成的随机数
    const generatedNumbers = new Set<number>(); // 使用Set来自动去重
    const tempArray: number[] = []; // 临时数组,用于辅助生成不重复的随机数

    // 如果不允许重复,则使用去重算法生成随机数
    if (!this.isUnique) {
      // 如果请求的随机数数量超过了范围内的总数,则显示错误提示
      if (countValue > range) {
        this.getUIContext().showAlertDialog({
          title: '错误提示',
          message: `请求的随机数数量超过了范围内的总数`,
          confirm: {
            defaultFocus: true,
            value: '我知道了',
            fontColor: Color.White,
            backgroundColor: this.primaryColor,
            action: () => {} // 点击确认后的回调
          },
          onWillDismiss: () => {}, // 对话框即将关闭时的回调
          alignment: DialogAlignment.Center, // 对话框的对齐方式
        });
        return;
      }

      for (let i = 0; i < countValue; i++) {
        let randomIndex = Math.floor(Math.random() * (range - i)); // 在剩余范围内选择一个随机索引
        let randomNum = 0;
        if (tempArray[randomIndex] !== undefined) { // 如果索引位置已有值,则使用该值
          randomNum = tempArray[randomIndex];
        } else {
          randomNum = startValue + randomIndex; // 否则计算新的随机数
        }
        generatedNumbers.add(randomNum); // 添加到Set中,自动去重
        if (tempArray[range - 1 - i] === undefined) { // 更新末尾元素的位置
          tempArray[range - 1 - i] = startValue + range - 1 - i;
        }
        tempArray[randomIndex] = tempArray[range - 1 - i]; // 将末尾元素移到随机位置
      }
      // 将生成的随机数转换成JSON格式的字符串
      this.generatedNumbers = JSON.stringify(Array.from(generatedNumbers));
    } else {
      // 如果允许重复,则直接生成随机数
      for (let i = 0; i < this.countValue; i++) {
        let randomNumber = this.startValue + Math.floor(Math.random() * (this.endValue - this.startValue));
        tempArray.push(randomNumber);
      }
      // 将生成的随机数转换成JSON格式的字符串
      this.generatedNumbers = JSON.stringify(tempArray);
    }
  }

  // 将生成的随机数复制到剪贴板的方法
  private copyToClipboard(text: string): void {
    const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据
    const systemPasteboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板
    systemPasteboard.setData(pasteboardData); // 设置剪贴板数据
    // 显示复制成功的提示信息
    promptAction.showToast({ message: '已复制' });
  }

  // 构建页面布局的方法
  build() {
    Column() {
      // 标题栏,展示应用名
      Text("随机数生成")
        .width('100%') // 设置宽度为100%
        .height(54) // 设置高度为54
        .fontSize(18) // 设置字体大小为18
        .fontWeight(600) // 设置字体粗细为600
        .backgroundColor(Color.White) // 设置背景颜色为白色
        .textAlign(TextAlign.Center) // 设置文本居中对齐
        .fontColor(this.fontColor); // 设置文本颜色

      // 随机数范围设置区域
      Column() {
        Row() {
          Text(`随机数范围`)
            .fontWeight(600) // 设置字体粗细为600
            .fontSize(18) // 设置字体大小为18
            .fontColor(this.fontColor); // 设置文本颜色
        }
        .margin({ top: `${this.baseSpacing}lpx`, left: `${this.baseSpacing}lpx` }); // 设置边距

        // 输入随机数范围的两个值
        Row() {
          TextInput({ placeholder: '开始(>=)' }) // 输入框,显示占位符
            .layoutWeight(1) // 设置布局权重为1
            .type(InputType.Number) // 设置输入类型为数字
            .placeholderColor(this.isFocusStart ? this.primaryColor : Color.Gray) // 设置占位符颜色
            .fontColor(this.isFocusStart ? this.primaryColor : this.fontColor) // 设置文本颜色
            .borderColor(this.isFocusStart ? this.primaryColor : Color.Gray) // 设置边框颜色
            .borderWidth(1) // 设置边框宽度
            .borderRadius(10) // 设置圆角半径
            .backgroundColor(Color.White) // 设置背景颜色
            .showUnderline(false) // 不显示下划线
            .onBlur(() => this.isFocusStart = false) // 输入框失去焦点时的处理
            .onFocus(() => this.isFocusStart = true) // 输入框获得焦点时的处理
            .onChange((value: string) => this.startValue = Number(value)); // 输入值变化时的处理

          // 分隔符
          Line().width(10) // 设置分隔符宽度

          TextInput({ placeholder: '结束(<=)' }) // 输入框,显示占位符
            .layoutWeight(1) // 设置布局权重为1
            .type(InputType.Number) // 设置输入类型为数字
            .placeholderColor(this.isFocusEnd ? this.primaryColor : Color.Gray) // 设置占位符颜色
            .fontColor(this.isFocusEnd ? this.primaryColor : this.fontColor) // 设置文本颜色
            .borderColor(this.isFocusEnd ? this.primaryColor : Color.Gray) // 设置边框颜色
            .borderWidth(1) // 设置边框宽度
            .borderRadius(10) // 设置圆角半径
            .backgroundColor(Color.White) // 设置背景颜色
            .showUnderline(false) // 不显示下划线
            .onBlur(() => this.isFocusEnd = false) // 输入框失去焦点时的处理
            .onFocus(() => this.isFocusEnd = true) // 输入框获得焦点时的处理
            .onChange((value: string) => this.endValue = Number(value)); // 输入值变化时的处理
        }
        .margin({
          left: `${this.baseSpacing}lpx`, // 左边距
          right: `${this.baseSpacing}lpx`, // 右边距
          top: `${this.baseSpacing}lpx`, // 上边距
        });

        // 输入生成随机数的个数
        Text('生成随机数个数')
          .fontWeight(600) // 设置字体粗细为600
          .fontSize(18) // 设置字体大小为18
          .fontColor(this.fontColor) // 设置文本颜色
          .margin({ left: `${this.baseSpacing}lpx`, top: `${this.baseSpacing}lpx` }); // 设置边距

        Row() {
          TextInput({ placeholder: '' }) // 输入框,显示占位符
            .layoutWeight(1) // 设置布局权重为1
            .type(InputType.Number) // 设置输入类型为数字
            .placeholderColor(this.isFocusCount ? this.primaryColor : Color.Gray) // 设置占位符颜色
            .fontColor(this.isFocusCount ? this.primaryColor : this.fontColor) // 设置文本颜色
            .borderColor(this.isFocusCount ? this.primaryColor : Color.Gray) // 设置边框颜色
            .borderWidth(1) // 设置边框宽度
            .borderRadius(10) // 设置圆角半径
            .backgroundColor(Color.White) // 设置背景颜色
            .showUnderline(false) // 不显示下划线
            .onBlur(() => this.isFocusCount = false) // 输入框失去焦点时的处理
            .onFocus(() => this.isFocusCount = true) // 输入框获得焦点时的处理
            .onChange((value: string) => this.countValue = Number(value)); // 输入值变化时的处理
        }
        .margin({
          left: `${this.baseSpacing}lpx`, // 左边距
          right: `${this.baseSpacing}lpx`, // 右边距
          top: `${this.baseSpacing}lpx`, // 上边距
        });

        // 设置数字是否可重复的开关
        Row() {
          Text('数字是否可重复')
            .fontWeight(400) // 设置字体粗细为400
            .fontSize(16) // 设置字体大小为16
            .fontColor(this.fontColor) // 设置文本颜色
            .layoutWeight(1); // 设置布局权重为1

          Toggle({ type: ToggleType.Checkbox, isOn: this.isUnique }) // 切换按钮
            .width('100lpx') // 设置宽度
            .height('50lpx') // 设置高度
            .borderColor(Color.Gray) // 设置边框颜色
            .selectedColor(this.primaryColor) // 设置选中时的颜色
            .onChange((isOn: boolean) => this.isUnique = isOn) // 切换状态变化时的处理
            .align(Alignment.End); // 设置对齐方式为右对齐
        }
        .margin({
          top: `${this.baseSpacing}lpx`, // 上边距
        })
        .width('100%') // 设置宽度为100%
        .padding({
          left: `${this.baseSpacing}lpx`, // 左内边距
          right: `${this.baseSpacing}lpx`, // 右内边距
          top: `${this.baseSpacing / 3}lpx`, // 上内边距
        })
        .hitTestBehavior(HitTestMode.Block) // 设置点击测试行为
        .onClick(() => this.isUnique = !this.isUnique); // 点击时切换状态

        // 生成随机数的按钮
        Text('开始生成')
          .fontColor(Color.White) // 设置文本颜色为白色
          .backgroundColor(this.primaryColor) // 设置背景颜色为主题色
          .height(54) // 设置高度为54
          .textAlign(TextAlign.Center) // 设置文本居中对齐
          .borderRadius(10) // 设置圆角半径
          .fontSize(18) // 设置字体大小为18
          .width(`${650 - this.baseSpacing * 2}lpx`) // 设置宽度
          .margin({
            top: `${this.baseSpacing}lpx`, // 上边距
            left: `${this.baseSpacing}lpx`, // 左边距
            right: `${this.baseSpacing}lpx`, // 右边距
            bottom: `${this.baseSpacing}lpx` // 下边距
          })
          .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 设置点击效果
          .onClick(() => this.generateRandomNumbers()); // 点击时生成随机数
      }
      .width('650lpx') // 设置宽度
      .margin({ top: 20 }) // 设置上边距
      .backgroundColor(Color.White) // 设置背景颜色为白色
      .borderRadius(10) // 设置圆角半径
      .alignItems(HorizontalAlign.Start); // 设置水平对齐方式为左对齐

      // 显示生成的随机数
      Column() {
        Text(`生成的随机数为:`)
          .fontWeight(600) // 设置字体粗细为600
          .fontSize(18) // 设置字体大小为18
          .fontColor(this.fontColor) // 设置文本颜色
          .margin({
            top: `${this.baseSpacing}lpx`, // 上边距
            left: `${this.baseSpacing}lpx`, // 左边距
          });

        Text(`${this.generatedNumbers}`) // 显示生成的随机数
          .width('650lpx') // 设置宽度
          .fontColor(this.primaryColor) // 设置文本颜色为主题色
          .fontSize(18) // 设置字体大小为18
          .textAlign(TextAlign.Center) // 设置文本居中对齐
          .padding({ left: 5, right: 5 }) // 设置内边距
          .margin({
            top: `${this.baseSpacing / 3}lpx` // 上边距
          });

        // 复制生成的随机数到剪贴板的按钮
        Text('复制')
          .enabled(this.generatedNumbers ? true : false) // 按钮是否可用
          .fontColor(Color.White) // 设置文本颜色为白色
          .backgroundColor(this.primaryColor) // 设置背景颜色为主题色
          .height(54) // 设置高度为54
          .textAlign(TextAlign.Center) // 设置文本居中对齐
          .borderRadius(10) // 设置圆角半径
          .fontSize(18) // 设置字体大小为18
          .width(`${650 - this.baseSpacing * 2}lpx`) // 设置宽度
          .margin({
            top: `${this.baseSpacing}lpx`, // 上边距
            left: `${this.baseSpacing}lpx`, // 左边距
            right: `${this.baseSpacing}lpx`, // 右边距
            bottom: `${this.baseSpacing}lpx` // 下边距
          })
          .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 }) // 设置点击效果
          .onClick(() => this.copyToClipboard(this.generatedNumbers)); // 点击时复制随机数
      }
      .width('650lpx') // 设置宽度
      .backgroundColor(Color.White) // 设置背景颜色为白色
      .borderRadius(10) // 设置圆角半径
      .margin({ top: `${this.baseSpacing}lpx` }) // 设置上边距
      .alignItems(HorizontalAlign.Start); // 设置水平对齐方式为左对齐
    }
    .height('100%') // 设置高度为100%
    .width('100%') // 设置宽度为100%
    .backgroundColor("#f2f3f5"); // 设置背景颜色
  }
}


PrefPPO 首次(?)出现在 PEBBLE,作为 pebble 的一个 baseline,是用 PPO 复现 Christiano et al. (2017) 的 PbRL 算法。

For evaluation, we compare to Christiano et al. (2017), which is the current state-of-the-art approach using the same type of feedback. The primary differences in our method are (1) the introduction of unsupervised pre-training, (2) the accommodation of off-policy RL, and (3) entropy-based sampling. We re-implemented Christiano et al. (2017) using the state-of-the-art on-policy RL algorithm: PPO (Schulman et al., 2017). We use the same reward learning framework and ensemble disagreement-based sampling as they proposed. We refer to this baseline as Preference PPO.

Christiano et al. (2017) 这篇文章的题目是 Deep reinforcement learning from human preferences,发表在 NeurIPS 2017;arxiv:
https://arxiv.org/abs/1706.03741
,GitHub:
https://github.com/mrahtz/learning-from-human-preferences(用
TensorFlow 实现的)。

01 论文阅读:Deep reinforcement learning from human preferences

1.1 intro

intro:

  • 大规模应用 RL 的限制是,许多任务涉及复杂、定义不明确或难以指定的目标。(举了一些例子)
  • 如果有人类 demo(专家数据),可以 inverse RL 或 behavior cloning,但是很多任务难以给出人类 demo,比如控制奇形怪状的 跟人类很不像的机器人。
  • 我们的 PbRL 思路:从 human feedback 中学习 reward model,当作 RL 中的奖励函数。这可以解决上面的两个问题,允许 non-expert user 来给 feedback,并且(据论文说)可以节省一个数量级的 feedback 数量。
  • human feedback 的形式:让 human 看两段 video,指出哪一段更好(即 preference)。

Related Work:

  • 列举了很多从 human 打分或排序中学 RL 的工作。还有一些工作在 RL 之外的 setting 使用 preference。
  • Akrour 2012 和 2014 年的工作,貌似也算是 PbRL,但他们的方法是在整个 trajectory 上打 preference,不像我们只需要比较两个较短的 video segment。
    • Akrour 2012:April: Active preference learning-based reinforcement learning. Joint European Conference on Machine Learning and Knowledge Discovery in Databases(好像是 ECML PKDD,不知道是什么概念)2012。
    • Akrour 2014:Programming by feedback. ICML 2014。
  • 从 feedback 里学 reward model 的方法,貌似跟 Wilson et al. (2012)工作比较像。
    • Wilson et al. (2012):A Bayesian approach for policy learning from trajectory preference queries. NeurIPS 2012。

1.2 Method

首先介绍了一下 PbRL 的 setting:

  • 定义 segment
    \(\sigma = ((s_0, a_0), ... (s_{k-1}, a_{k-1}))\)
    ,是长为 k 的轨迹段。
  • 定义 preference
    \(\sigma_0\succ\sigma_1\)
    表示轨迹段 σ0 比 σ1 更好。接下来,我们用这些 preference 数据学出 reward model。

然后是 method:(发现这篇文章好像没给伪代码)

  • 我们要维护一个 reward model
    \(\hat r\)
    ,和一个 policy
    \(\pi\)
  • 大概重复这样的过程:
    • 用目前的 policy
      \(\pi\)
      生成 query
      \((\sigma_0,\sigma_1)\)
    • 把 query 给 human 比较得到 preference 数据;
    • 用新 preference 数据学 reward model;
    • 把 reward model 当作奖励函数,去学 RL、更新 policy
      \(\pi\)

① 把 reward model 当作奖励函数,去学 RL、更新 policy
\(\pi\)

  • 论文声称
    \(\hat r\)
    可能是非平稳的(因为 reward model 一直在更新),所以他们想用 policy gradient 方法,因为 policy gradient 方法对奖励函数具有鲁棒性。
  • 他们用 A2C(advantage actor-critic)做 Atari,用 TRPO 做 MuJoco。调整了 TRPO 的 entropy bonus,MuJoCo 的 swimmer 任务使用 0.001 的 entropy bonus,其他任务使用 0.01 的 entropy bonus。
  • 把 reward model 产生的
    \(\hat r\)
    归一化到 0 均值 1 标准差。

② 用目前的 policy
\(\pi\)
生成 query
\((\sigma_0,\sigma_1)\)

  • preference 数据的形式是
    \((\sigma_0,\sigma_1, p)\)
    ,其中 p 是一个 {0,1} 上的分布。
  • 如果 human 能打出 preference,那么 p = 0 或 1。如果 human 分不出来,则 p 是 01 上的均匀分布。如果 human 感觉 query 是不可比的,那么不会使用这个 query 学 reward model。

③ 用新 preference 数据学 reward model:

  • 我们使用 Bradley-Terry model 来建模
    \(\hat r\)
    和 preference 之间的关系:


  • \[\hat P[\sigma_0\succ \sigma_1] = \frac{\exp\sum\hat r(s_t^0,a_t^0)}
    {\exp\sum\hat r(s_t^0,a_t^0) + \exp\sum\hat r(s_t^1,a_t^1)} ~~.
    \tag{1}
    \]

  • 然后,我们去优化 cross-entropy loss:


  • \[L(\hat r) = -\sum_{(\sigma_0,\sigma_1,p)} \left(
    p(0)\log \hat P[\sigma_0\succ \sigma_1] + p(1)\hat P[\sigma_1\succ \sigma_0]\right)
    \tag{2}
    \]

  • (以上流程已经变成经典的 PbRL 做法)

  • 他们还加了三个小 trick:


    • 对 reward model 进行 ensemble,学 n 个独立的 reward model,用它们每个人单独归一化后的值 取平均 作为
      \(\hat r\)
      的值。
    • 把一部分 preference 数据拿出来做验证集,以调整神秘的 L2 正则化的权重参数,并添加神秘 dropout,以避免 reward model 过拟合(?)
    • label smoothing(?)貌似是当 human 打出 p = 0 的时候,认为 p = 0 的概率是 0.95,p = 1 的概率是 0.05。

query selection:

  • 即,我们现在有很多 trajectory,要从里面截出 segment、组成 segment pair,作为 query 送给人去比较。应该如何选取 segment pair 作为 query?
  • 这里使用了基于 disagreement 的 query selection,貌似是让每个 reward model 给出
    \(\hat P[\sigma_0\succ \sigma_1]\)
    的值,计算这些值的方差,然后选一个方差最大的 query。

1.3 实验结果

算法是在 TensorFlow 中写的。Github:
https://github.com/mrahtz/learning-from-human-preferences(貌似不是官方代码…)

  • preference 一些是人类打的,另一些是 scripted teacher 打的。
    • Appendix B 中有让人类打 preference 的一些 prompt,感觉很有趣。
    • scripted teacher:上面公式 (1) 中用任务的真 reward 替换
      \(\hat r\)
      ,反向生成 preference。
  • MuJoCo 实验中,分别使用真 reward、1400 700 350 个 scripted teacher queries、和 750 个 human queries。
    • (个人理解,这里的 750 human queries 包含的 label 少于 750 个,因为人类认为不可比的 query 应该会直接扔掉)
    • MuJoCo 实验中,很多 task 都做了 1e7 步,相比 pebble 来说学的很慢;pebble 1e6 步就能学出来。
  • Atari 实验中,分别使用真 reward、10k 5.6k 3.3k 个 scripted teacher queries、和 5.5k 个 human queries。(好多 human label…… 这要打好久好久;如此充足的实验,真是 solid work 呀)
  • 这些实验没有得出 human label 比 scripted teacher 好用的结论,论文说,可能是因为 human 犯错、不同 human 的打标签准则不一样等原因。
  • 一些 Appendix A 里的实验细节:
    • 有些环境会提前结束 episode,比如当 agent 倒下、死亡或到达目的地(?)他们声称这种 done 信号可能会提供 task 信息,因此魔改了环境,规避掉了 episode 长度可变的情况(?)使得 agent 死亡时得到一个大惩罚,或者 agent 到达目的地的时候直接 reset env 而不 done(?)
    • 在 MuJoCo 中,在实验开始前直接用随机 policy 打 25% 的 queries,然后以一个随时间递减的 rate 来打 preference;segment length = 15 - 60,取决于具体 task。
    • 在 Atari 中,reward model 和 policy 的输入是一样的,都是一个处理图像的卷积网络(无端联想,听说 DPO 的主要贡献是可以省掉 LLM RLHF 的 reward model,因为 reward model 应该跟 policy 一样大,所以省掉它可以节约很多显存)。
    • Atari 其实跑的是 A3C,在实验开始前打 500 个 query,segment length = 25。
  • 3.2 节还在 MuJoCo 里学习了 hopper 后空翻的 novel behavior,据文章说,可以保证后空翻后 hopper 脚着地。使用 900 个 human query 学习的。3.2 节还有其他的 novel behavior。
  • 3.3 节做了非常充分的 ablation study。发现 segment 长度 = 1 貌似会性能变差,reward model 不加正则化影响不大,把 query selection 改成随机好像也影响不大,以及,最好边更新 policy 边拿最新轨迹打 preference。

02 PEBBLE 中的 PrefPPO 实现

PEBBLE 中的 PrefPPO 实现,直接魔改了 stable_baselines3 的 PPO 模型;他们写了一个叫做 PPO_REWARD 的新类,把所有跟 reward model 的交互都封装到 model.learn() 函数里了。

2.1 reward model 如何构建

跟 pebble 的 reward model 一样。

如果 state 和 action 都是了连续的(比如普通的 cheetah walker),那么就把 state 和 action concat 起来,作为 reward model 的输入。

如果 state 是图像,action 是离散的(比如 Christiano 2017 论文中的 Pong 环境),那么(按照 Christiano 2017 复现代码),…… 好像直接拿 state 图像来算 reward 了,没有 concat 一个 one-hot action 或者数值 action。

2.2 PPO_REWARD 的 model.learn()

PPO 的大概流程:收集 rollout → 计算 rollout 的 advantage 之类 → 计算 loss 并 backward → 收集新 rollout…

在收集 rollout 的过程中,PrefPPO 把要收集的真实 task reward 替换成了
\(\hat r\)
,并把 rollout 数据都添加到 query 的备选中。

在收集 rollout 前后,貌似都有调用 learn_reward() 函数来训练 reward model,这个函数首先收集 query,然后拿收集的 query 学习 reward model。跟 pebble 基本一样。





1. 什么是自动化项目搭建

当一个软件开发工程师接到一个新的Web项目开发的时候,往往需要完成一些准备工作,例如,选择web框架,项目的目录结构设计,数据库的连接配置,Redis/Kafka连接和配置;甚至包括一些基础功能的实现和封装,例如 MySQL库增删查改操作的封装,登录功能,以及登录token的验证。这个过程通常称为
项目初始化

项目搭建

当我们的大部分项目都会用到这些基础功能,我们会将这个搭建好的项目放到一个单独的代码仓库,当需要开发新的项目时就从从这个仓库拉取代码,在这个项目代码的基础上继续开发。这项目通常被称为
种子项目

模板项目

每次从模板项目拉取代码,都需要手动修改
模板项目
的名称,例如:将
template-project
改为
company-user-project

company-payment-project
。甚至在使用
模板
项目的时候,会有个性化的需求,
company-user-project
需要使用MySQL数据库,
company-payment-project
需要使用MongoDB数据库,我们可以进一步实现脚手架工具。通过可选配置的方式生成个性化的
模板项目
,这样的工具我们通常称为
脚手架工具

图:Spring initializr

自动化测试工程师在接到新需求时,也需要完成类似项目初始化的工作(例如选择测试框架、设计目录结构、集成测试报告,以及各种主流的测试库等)。此外,也包括实现一些基础用例或封装一些通用操作,比如系统的注册、登录用例,封装随机数的生成等等。毫无疑问,这个过程也被认为是
自动化项目搭建

当我们搭建好了自动化测试项目,同样可以将其作为
模板项目
使用。然后,基于
模板项目
,我们可以更加快速的编写自动化测试用例。

为什么要介绍这个概念,是因为网上看到大量的文章将
自动化测试项目搭建
叫做
自动化测试框架开发
,这显然是错误的认知,因为两者的角度和目的是不同的。

  • 自动化测试项目搭建: 服务于公司具体业务,为了更快速地编写业务的测试用例。

  • 自动化测试框架开发: 为了解决一类通用问题,开发设计的一种通用的能力,从而定义解决问题的方法和结构。



2. 为什么设计自动化测试框架

开发框架的原因可以有多种角度,以下是比较常见的原因。

2.1 提高开发效率

现有框架可能存在以下问题:

  • 现有框架使用过程中存在过多的第三方依赖,安装和配置比较繁琐。
  • 使用复杂,测试工程师需要花费过多时间学习或适配。

通过开发更贴合需求的自动化测试框架,可以减少重复性劳动,提升开发效率。

2.2.满足特定类型需求

现有框架存在无法满足特定类型的业务的测试需求(例如,gRPC、Kafka的测试用例编写),需要在现有框架的基础上做更多的功能扩展和封装。

通过开发功能更加强大的框架,更好的解决现有业务类型的自动化测试需求。

2.3 提供更优的设计理念

提供更优的设计理念或创新技术实现。

  • 创新性设计:基于新的架构或设计模式提供更高性能、更易扩展的解决方案。
  • 领域驱动:专注于某一特定领域(如关键字驱动,数据驱动、方法链)的最佳实践。

2.4.提升团队协作

规范团队协作,提升开发体验和代码一致性。

  • 框架可以规范团队的开发方式,减少个性化差异带来的协作成本。
  • 提供标准化的工具链、模块和流程,确保团队代码质量和一致性。



3. 自动化测试框架设计的方向

当我们决定去设计自动化测试框架,那么可以有两个方向:
从零开始设计

基于单元测试框架二次开发

3.1 从零开始设计

从零开始设计自动化测试框架,例如针对一款的编程语言,单元测试框架一般需要作为基础库被设计并集成到编程语言中。

标准化的测试结构

单元测试框架提供了一种统一的结构化方式,让开发者以一致的方式组织和运行测试。

  • 生命周期方法
    : 通常框架会定义一套标准化的生命周期钩子,用于在每个测试方法执行前后进行资源管理:
    setup/beforeEach
    : 在每个测试运行之前初始化测试环境(如创建测试数据、实例化对象)。
    teardown/afterEach
    : 在每个测试运行后清理资源(如关闭数据库连接、删除临时文件)。

  • 测试方法的命名
    : 通常有特定约定,比如以
    test_
    开头(Python 的 unittest)或
    @Test
    注解(Java 的 JUnit)。 这种约定使得框架可以自动发现测试方法。

  • 独立性
    : 每个测试方法应保持独立,不依赖其他测试。测试之间的隔离有助于更快定位问题。

断言机制

断言(Assertions)是单元测试的核心,用于验证被测代码的行为是否符合预期。

断言的作用:

  1. 通过对输入和输出的验证,确保代码逻辑正确。
  2. 如果断言失败,测试会立即终止并报告错误。

测试发现

测试发现是单元测试框架的一项重要功能,能够自动找到符合规范的测试。

框架会扫描特定的模块或文件夹,找到符合命名约定的方法。例如:在 Python 中,pytest 会自动发现以
test_
开头的方法。在 Java 的 JUnit 中,
@Test
注解标识的方法会被识别为测试方法。

测试套件和测试运行器

测试套件和运行器使得开发者可以高效地组织和执行测试。

  • 测试套件(Test Suite)

测试套件是一个集合,用于将多个测试用例组合在一起运行。方便地对一组相关的测试进行分组管理。

  • 测试运行器(Test Runner)

测试运行器负责执行测试并收集测试结果。

测试报告和结果反馈

测试报告用于展示测试的执行结果,帮助开发者快速了解代码的健康状态。测试结果分类

  • 通过(Pass): 测试正常完成且所有断言成功。
  • 失败(Fail): 测试未通过,某个断言失败。
  • 错误(Error): 测试执行过程中抛出了未预期的异常。
  • 跳过(Skip/Ignore): 测试因某些条件未被执行。

扩展:xUnit 被认为是许多主流编程语言的单元测试框架的雏形和灵感来源。xUnit 是一种架构模式,最早由 Kent Beck 和 Erich Gamma 在 SUnit(Smalltalk 的单元测试框架)中提出,并成为了测试框架设计的标准。其设计思想和概念被广泛应用到其他语言中,例如 JUnit(Java)、NUnit(.NET)、pytest(Python)等。

3.2 基于单元测试框架二次开发

基于单元测试框架二次开发,在单元测试框架的基础上,更偏注重于扩展各种测试能力。

通常,单元测试框架已经提供了基础的测试能力,为了更好的支撑各种类型的测试,我们可以在此基础上进行扩展,以便于满足不同类型的需求。

基于单元测试框架二次开发的方向比较多,取决于基于框架设计的定位和目标。以下是常见的扩展功能。

数据驱动

数据驱动是自动化测试最常见的功能之一,可以有效的减少样例代码的编写,从而提高测试用例编写的效率。

  • 数据驱动装饰器

可以通过数据驱动装饰器来驱动测试测试用例(方法), 例如,Seldom框架的
@data([])
装饰器。

  • 数据驱动文件

通过取数驱动文件读取不同类型的数据文件。例如,Seldom框架的
@data_file("./data/file.json")
管理测试数据。

定制化测试报告

测试报告是自动化测试框架的非常重要的功能,我们需要对报告做一些定制化开发。

  • 个性化测试样式和内容

例如显示公司logo、人员个名称和角色等。

  • 生成不同的报告类型

不同的运行模式需要不同的报告类型。例如,本地执行需要HTML格式的报告,CI/CD 或平台化执行需要XML、JSON格式的测试报告。

脚手架工具

集成脚手架工具,可以快速的生成
自动化测试项目模板

关于自动化测试项目模板,文章的开头已经介绍,这里不再阐述。

集成消息功能

每个公司都有自己的通讯工具,邮件、钉钉、企微、飞书等。 通过调用相关工具的API,实现发消息功能,可以让测试的运行结果更快的发送给相关人员。

集成各种测试库

基于框架的定位,可以集成不同类型的测试库,并对这些库进行二次开发,使框架的使用更加高效统一。

  • Web UI测试

如果是为了实现Web UI自动化测试,那么可以集成 Selenium、Playwright等测试库,并对这些库的API进行二次封装。

  • API 测试

如果是为支持接口测试,可以集成 Requests、webSocket、gRPC等测试库,并对这些库的API进行二次封装。

  • App UI测试

如果是为了实现App UI自动化测试,那么可以集成 Appium、uiautomator2 等测试库,并对这些库的API进行二次封装。



4. 自动化测试框架设计基本准则

4.1 独立的名字和版本管理

我们应该把框架当成一个独立的项目来进行开发、维护和升级。

框架的命名

首先,应该为框架起一个独立的名字,既可以以某个动物或植物命名,比如,Python(蟒蛇)或Lettuce(生菜);也可以按照框架的本意命名,比如, Robot Framework(自动化框架)或unittest(单元测试);还可以是缩写合成词,比如,pytest = Python + Test、Appium = Application + Selenium等,关键是简单好记。

版本号管理

其次,框架应该有自己的版本号,推荐使用GNU风格的版本号命名。

格式:
主版本号.子版本号[.修正版本号[.编译版本号]]

  • 主版本号:重构版本。
  • 子版本号:重大功能改进。
  • 修正版本号:小升级或者bug修复。
  • 编译版本号:一般是编译器在编译过程中自动生成的,我们只定义其格式,并不进行人为的控制。

独立的安装

最后,框架应该提供独立的安装,比如,Python使用
pip
命令进行安装。

对于开源的项目来说,例如,需要创建setup.py或pyproject.toml打包文件,将项目打包成.whl格式的文件,提交到pypi.org官方仓库。

4.2 具备通用性

作为一款框架,其定位和目标一定是解决一类通用问题并提供能力。

例如:
数据驱动

自动化发邮件

生成随机数

数据缓存

命令行工具
这些都与具体公司业务无关,提供的是通用的能力。

4.3 清晰的定位和目标

自动化测试框架被设计的初衷一定是为更好的了解决某一类问题。在设计之初,我们应该有一个清晰的目标和定位。

从无到有地解决一类问题

xUnit在单元测试框架领域具有开创性意义。前文有对 xUnit 进行介绍。

更加简单地解决一类问题

Flask是一个使用Python编写的轻量级Web框架,通过它,我们可以只简单地编写几行代码就搭建一个Web服务。

提供更加强大且丰富的功能

Django是一个开放源代码的由Python编写的Web框架。

Django虽然学习成本较高,但是它功能提供了 ORM(关系对象映射)、Admin管理系统、模板系统、Cache系统、表单处理、会话(session)、国际化等,这些功能几乎都是开箱即用的,可以用来实现一个较为复杂的系统。

最后,当你要设计一个自动化测试框架的时候,不妨思考一下,设计目标是什么?为了解决什么问题?是否已经有更好的开源框架可以直接使用。



5. 相关书籍推荐

那么,是否有一本书能讲清楚
自动化测试框架设计
?

答案是:《自动化测试框架设计》一书

本书由虫师编著,作为 SeldomQA GitHub千星开源项目的开发者,在
自动化测试框架设计

定制化测试报告设计

设计模式
,以及
测试平台开发
方面有着深厚技术积累和独特的设计理念。

书中浅显易懂的介绍了 SeldomQA 相关项目中的诸多设计和封装技术。并且,介绍了一个开源自动化测试框架从设计到发布的整个流程。

此外,书中还介绍了如何打通
自动化框架

自动化测试平台
,这是一种独特的技术方案,为自动化测试平台提供了新的设计思路。

最后,结合当下热门的AI技术,作者还介绍AI在自动化领域的探索方向。

【环境】kos5.8sp2, kernel5.10

最近工作中需要搭建一个软件环境,其依赖的 glibc 版本较高,因此在安装时给出了以下错误:

xxx: /lib64/libc.so.6: version 'GLIBC_2.33' not found (required by xxx)

去查看当前机器的 libc.so 支持的 GLIBC 版本,发现确实太低了:

strings /usr/lib64/libc.so.6 | grep GLIBC

而且,难受的是,这个需要安装的软件仅仅提供了一个二进制安装程序,没办法基于其源码做定制化改动。

这样看来,不得不升级当前系统的 glibc 了。

网上有很多有关替换 glibc 的教程,大都是给出了
make && make install
的方案。
然而这种方案是及其风险的。
因为 glibc 是系统的核心库之一,几乎所有的用户空间程序都依赖于它。如果不考虑风险直接
make install
,那么当新的 glibc 安装成功后,你的系统大概率会挂掉。一个有代表性的现象是,你执行一些诸如
ls
的简单 shell 指令都会报错了。

其实无痛安装 glibc 有更好的办法,那就是基于 glibc 的 rpm 源码包在本地环境编译成 rpm,再进行安装。

我的当前系统为 kos5.8SP2,与 RHEL 同根同源。因此,我找了一个 Fedora 的 glibc 安装包:
glibc-2.38-19.fc39.src.rpm
,下面开始编译。

mkdir glibc-2.38 && cd glibc-2.38

# 拆分 src.rpm
rpm2cpio ../glibc-2.38-19.fc39.src.rpm | cpio -div

# 手动拷贝到 rpmbuild/SOURCE
cp -r * ~/rpmbuild/SOURCE/

# 进入源码目录
cd ~/rpmbuild/SOURCE/

# 安装依赖
yum builddep glibc.spec

# 开始编译
rpmbuild -ba glibc.spec --nodebuginfo

编译成功后,会在
~/rpmbuild/RPMS/
目录下生成 rpm,安装即可:

cd ~/rpmbuild/RPMS/ && yum install *

安装成功后,验证一下,glibc 已经更新了:

1.简介

前面几篇宏哥介绍了两种(java和maven)环境搭建和浏览器的启动方法,这篇文章宏哥将要介绍第一个自动化测试脚本。前边环境都搭建成功了,浏览器也驱动成功了,那么我们不着急学习其他内容,首先宏哥搭建好的环境中创建首个完整的自动化测试脚本,让小伙伴或者童鞋们提前感受感受,也是为了激起大家的学习兴趣。

宏哥的个人经验是:自动化脚本编写比较容易,最大的困难去如何去写测试断言。自动化测试,最重要的还是落在测试上面,而不是自动化,自动化只是手段。断言的写法,就考验出一个测试工程师是否考虑全面,体现出你的用例编写水平。

2.测试用例

很多童鞋或者小伙伴们可能会有这样有个误区:自动化测试是不需要设计测试用例的。其实不然它也是需要设计测试用例,然后根据用例进行脚本的编写和断言,只不过是把用例以代码的形式体现出来,而机器恰好可以识别代码,将代码跑起来,其实就是在执行你的用例,只不过是由机器帮你自动执行。好了废话少说开始说说宏哥今天要做的测试是:打开百度,输入北京-宏哥搜索,验证打开链接有没有北京-宏哥博客园的链接。

测试用例:打开百度首页,搜索:北京-宏哥,然后检查搜索列表,有没有 北京-宏哥 博客园链接。后续文章为了避免不必要的麻烦和错误,宏哥都在maven搭建的环境中进行实战演示。

2.1步骤

1.启动浏览器

2.打开百度首页:http://www.baidu.com

3.判断这个页面是不是我们提前知道的页面

4.定位搜索输入框,记录下输入框元素的id定位表达式:#kw

5.定位搜索提交按钮(百度一下),获取id定位表达式:#su

6.在搜索输入框输入:北京-宏哥,点百度一下这个按钮

7.在搜索结果列表去判断是否存在北京-宏哥博客园这个链接

8.退出浏览器,结束测试

2.2代码设计

2.3参考代码

packagecom.bjhg.playwright;importcom.microsoft.playwright.Browser;importcom.microsoft.playwright.BrowserType;importcom.microsoft.playwright.Locator;importcom.microsoft.playwright.Page;importcom.microsoft.playwright.Playwright;/***@author北京-宏哥
*
* @公众号:北京宏哥(微信搜索,关注宏哥,提前解锁更多测试干货)
*
* 《刚刚问世》系列初窥篇-Java+Playwright自动化测试- 5-创建首个自动化脚本(详细教程)
*
* 2024年7月12日
*/ public classLaunchChrome {

@SuppressWarnings(
"deprecation")public static voidmain(String[] args) {try (Playwright playwright =Playwright.create()) {//使用chromium浏览器,# 浏览器配置,设置以GUI模式启动Chrome浏览器(要查看浏览器UI,在启动浏览器时传递 headless=false 标志。您还可以使用 slowMo 来减慢执行速度。 Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(50));//创建page Page page =browser.newPage();//浏览器打开百度 page.navigate("https://www.baidu.com/");//判断title是不是 百度一下,你就知道 try{
String baidu_title
= "百度一下,你就知道";assert baidu_title ==page.title();
System.out.println(
"Test Pass");

}
catch(Exception e){
e.printStackTrace();
}
//定位搜索输入框,输入北京-宏哥 page.locator("#kw").type("北京-宏哥");//定位搜索提交按钮(百度一下)。点击 page.locator("#su").click();//这里通过元素XPath表达式来确定该元素显示在结果列表,从而判断Selenium官网这个链接显示在结果列表。 Locator ele_string=page.locator("//*[@id='1']/div/div[1]/h3/a");

String ele_string1
=ele_string.innerText();

System.out.println(ele_string1);
try{if(ele_string1.equals("北京-宏哥 - 博客园")){

System.out.println(
"Testing is successful!");

}
}
catch(Exception e){

e.printStackTrace();

}
//关闭page page.close();//关闭browser browser.close();
}
}

}

2.4运行代码

1.运行代码,右键Run AS->java Application,就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作。如下图所示:

3.小结

3.1equals和==的区别

宏哥这里简单的说一下,更详细的你可以百度查一下。关于判断两个字符串是否相等的问题。在编程中,通常比较两个字符串是否相同的表达式是“==”,但在java中不能这么写。在java中,用的是equals();

例:A字符串和B和字符串比较:
if(A.equals(B)){

}
返回true 或false.

String 的equals 方法用于比较两个字符串是否相等。由于字符串是对象类型,所以不能用简单的“==”判断。而使用equals比较两个对象的内容是否相等。

注意:equals()比较的是对象的内容(区分字母的大小写格式),但是如果使用“==”比较两个对象时,比较的是两个对象的内存地址,所以不相等。即使它们内容相等,但是不同对象的内存地址也是不相同的。

好了,今天时间也不早了,首个Java+Playwright的自动化测试脚本就分享到这里,感谢你耐心地阅读!