2024年7月

前几章我们讨论了RLHF的样本构建优化和训练策略优化,这一章我们讨论两种不同的RL训练方案,分别是基于过程训练,和使用弱Teacher来监督强Student

循序渐进:PRM & ORM

image.png

### 数据标注

想要获得过程监督所需的标注样本,其实是一件成本很高事情,因为需要对解题的每一个步骤是否正确进行标注。论文选择了3分类的label,positive是推理正确合理,negative是步骤错误或逻辑错误,neural是模糊或者存在误导。如下

image.png

为了让高昂的标注过程产生最大的价值,这里需要保证生成解题样本的格式规范(容易拆分成多个解题步骤),以及样本不能全是easy negative或者easy positive。也就是我们需要解决
推理格式

样本筛选
问题。

为了保证稳定的推理格式,这里论文训练了Generator,使用'\n'来分割每一步解题步骤。为了避免这一步微调导致样本信息泄露,论文使用few-shot构建格式正确的推理样本后,过滤了答案正确的样本,只使用答案错误但格式正确的样本训练Generator。更大程度保证微调只注入推理格式,不注入额外数学知识和推理信息。

在样本筛选步骤,论文使用当前最优的PRM模型筛选打分高,但是答案错误的
Convincing wrong answer
答案,来构建更难,过程监督信号更多,且PRM对当前解题过程一定存在至少一步判断错误的样本,来进行人工标注。

既然看到这里是使用PRM打分筛选样本来训练PRM,自然使用到了Iterated Training,也就是会先构建一波样本训练一个PRM,用新训练好的PRM,对问题的N个回答进行打分,再筛选Top K的Convincing wrong answer去进行人工标注,再训练PRM,这个过程总共迭代了10次。最终得到了
PRM800K解题步骤
的训练样本,包括从12K问题中采样得到的75K答案。

ORM的训练样本就简单很多,只需要用到问题的答案即可。不过考虑到上面PRM对样本进行了有偏筛选得到的大多是答案错误的样本,因此ORM的样本是在相同问题上用Generator重新随机生成的。所以ORM和PRM的回答样本并不相同。

训练和推理

训练阶段,ORM是预测最终答案正确与否的positive/negative分类目标;PRM的目标是预测每一个解题步骤的positive/neural/negative,这里论文没有做任何解题步骤前后关联,单纯
把每个解题步骤独立作为一个分类样本进行训练
,因此和ORM一样是分类任务。论文同时提到因为预训练LM和分类CLM的目标差异巨大,因此
低LR的训练得到的PRM更加稳定,不论模型大小都只训练2个Epoch

这里虽然感觉PRM这个每个解题步骤条件独立的假设有一些强,但确实如果按不独立来标注,那标注成本会再高一个数量级,也是不太现实~

推理阶段,论文给出了两种PRM的推理方案。一种是使用PRM计算每一步推理正确的概率,再把多个推理步骤的得分求积,得到每个答案唯一的打分,用来比较同一个问题多个回答之间的优劣。一种是预测第一个错误的步骤,这样PRM和ORM会先对可比,对于对的回答二者的预测都是全对,对于错的回答,二者的预测都是存在有错误的步骤,只不过PRM会进一步给出错误的具体位置。

效果

效果上论文使用Best-of-N的Major Voting作为基准,来对比PRM和ORM筛选答案的正确率,如下图,随着采样答案N的数量增加,PRM相比ORM和Major-Voting的相对优势会越来越显著。

image.png

考虑以上ORM和PRM训练数据集并不相同,不算严格的对比实验,之后论文还做了相对可比的消融实验,这里不再赘述。

除了直观的效果对比,PRM相比ORM还有几个对齐优势

  • redit Assignment
    :针对复杂问题PRM能提供错误具体产生的位置使得进一步的迭代修改,变得更加容易,因此PRM的奖励打分的边际价值更高
  • Safer
    :PRM针对COT的过程进行对齐,相比只对齐结果(可能存在过程错误)的一致性更高,个人感觉是reward hacking的概率会相对更低,因为对齐的颗粒度更细
  • negative Alignment Tax
    : 论文发现PRM似乎不存在对齐带来的效果下降,甚至还有效果提升。

青出于蓝:weak-to-strong

Clipboard_2024-06-25-08-21-38.png

weak-to-strong是openAI对齐团队在23年年终交出的答卷。论文的本质是对超级对齐问题进行一个简化的讨论,也就是当大模型的能力越来越强甚至超越人类的时候,人类的监督是否还能有效指导模型行为,保证模型的安全性和指令遵从性。以及这种弱监督该如何进行?

所以超级对齐本质是一个“弱-监督-强”的问题,而论文进行的简化,就是把人类监督超级模型的问题,类比简化成一个弱模型监督强模型的过程,即所谓“Weak-to-Strong Generalization”

论文的思路其实和前几年曾经火过的弱监督,半监督,带噪学习的思路非常相似。就是在任务标签上训练弱模型,然后使用训练后的弱模型进行打标,再使用模型打标的标签来训练强模型,看强模型的效果能否超越弱模型。逻辑上弱监督半监督,其实是提高模型在unseen样本上的泛化能力,而OpenAI这里研究的Weak-to-Strong更多是模型能的泛化。

论文可以分成两个部分,使用常规微调测试weak-to-strong的泛化效果,以及探索如何提升weak-to-strong的泛化,下面我们来分别说下

Experiment

首先论文选择了三种任务类型来测试模型泛化效果

  • NLP分类任务: 22个包括NLI,分类,CR,SA在内的NLP分类任务。这类任务可能大小模型表现都不错,模型越大效果会有提升但不明显
  • Chees Puzzles:象棋挑战预测下一步最佳下法的。这类任务可能有比较明显的模型规模效应,小模型做不了,得模型大到一定程度后效果会越来越好
  • ChatGPT Reward Model: 预测pair-wise的人类更偏好的模型回答。这类任务现在没啥模型效果好,大的小的都一般

其次就是在以上数据集上,分别进行以下训练

  • weak supervisor:使用以上数据训练小模型得到Teacher模型
  • Weak-to-strong:使用以上弱模型在held-out数据集上预测得到label,并使用这些弱监督的标签来训练一个更大更强的模型
  • strong ceiling:使用以上任务的样本直接训练强模型得到模型能力上限

以上得到的三个模型效果理论上应该是weak-supervisor < weak-to-strong < strong-ceiling,最后论文通过计算弱监督训练帮助强模型恢复的能力占比来衡量weak-to-strong监督的泛化效果,既Performance-Gap-Recovered(PGR)

Clipboard_2024-06-28-08-32-37.png

以下为直接微调的实验结果,下图分别展示了不同模型大小的strong student(横轴),weak teacher(颜色),在以上三个任务上的任务准确率,和对应的PGR用来衡量weak-to-strong的泛化效果。

  • NLP任务:最小的Teacher训练大许多倍的Student也能恢复20%以上的能力,随Teacher和Student的大小增大,PGR都会有提升
  • Chess Puzzle任务上,当Teacher模型较小的时候,Student几乎学不到任何信息。而随着Teacher变大,PGR会有显著提升,但是随着Student变大任务准确率和PGR反而会有下降。可能存在inverse scaling
  • ChatGPT Reward任务上,随着Teacher增大,Student的任务准确率会有提升,但泛化PGR几乎都不会超过20%。

Clipboard_2024-06-25-08-56-10.png

这里三种任务的泛化差异,其实和前面提到的三种任务本身难度,以及和模型大小的相关性有关。如果从噪声学习的角度来讨论的话,NLP任务和模型大小相关性低,且标签噪声较小;Chess Puzzle和单模型大小以及stduent-teacher之间的gap相关性都很大,teacher标签噪声,以及student-teacher预测的一致性都随模型scale变化;reward任务都很一般,和模型大小没啥关系。

整体上通过直接微调,能稳定获得一定的能力泛化(PGR>0),但泛化效果并不好。于是下面论文讨论了能否通过改变训练方案来提高weak-to-strong的泛化效果。

Improvement

方案一:Bootstraping

采用渐进训练的方案,也就是我们可以先用小模型对齐略大一些的模型,再用略大一些的模型来对齐更大一些的模型,然后逐步迭代下去。这种训练方式可能更适合上面Chess Puzzle的任务,考虑该任务存在Inverse Scaling,既当Student比Teacher大的越多,weak-to-strong的泛化效果越差,那我们可以逐步放大Student模型的大小,保持Teacher和Student之间的gap不要太大。

效果上不难发现对比以上的inverse scaling的PGR变化,在相同的Teacher模型上,我们可以获得几乎持平的PGR泛化效果,也就意味着更小的模型可以帮助更大的模型恢复相同比例(但绝对值更大) 的能力。这里论文统一采用了3次迭代,也就是会训练两个中间大小的模型。

Clipboard_2024-06-27-07-58-50.png

方案二:Regularization

如果我们想让大模型学习的时候,只学习小模型在任务上获得的Insight,而不是简单的去模仿小模型,可以通过加入正则项的方法。用的是半监督学习里面的最小熵原则,和Pseudo Label的损失函数是近似的,不熟悉的同学看这里
小样本利器3. 半监督最小熵正则

也就是在原始的交叉熵(左)的基础上,加上了student模型的预测熵值,这里f(x)是训练中的大模型,而t是一个动态阈值,是batch内样本预测概率的中位数,这样大模型即便不去学习Teacher模型,通过提高自己对预测样本的置信度(自信一点!你是对的),也可以降低损失函数。

\[Lconf(f) = (1 − α) · CE(f(x), fw(x)) + α · CE(f(x), \hat{f}_t(x))
\]

以上损失函数还可以改写为噪声损失函数中的Bootstrap Loss, 不熟悉的同学看这里
聊聊损失函数1. 噪声鲁棒损失函数
。也就是Student学习的label是由Teacher的预测label,和student模型自己预测的label混合得到的。逻辑也是一样,如果这个问题你对自己的预测很自信,那请继续自信下去!

\[Lconf(f) = CE(f(x), (1 − α) · fw(x) + α · \hat{f}_t(x))
\]

以上正则项的加入在NLP任务上,当student和teacher之间的gap较大时,能显著提高weak-to-strong的泛化效果,即便最小的Teacher也能恢复近80%的大模型效果,说明降低student无脑模仿teacher的概率是很有效的一种学习策略。

Clipboard_2024-06-27-08-25-08.png

Why Generalization

最后论文讨论了为何存在weak-to-strong泛化,以及在什么场景下存在。这是一个很大的问题,论文不可能穷尽所有的场景,因此有针对性的讨论了模仿行为和student模型本身对该任务是否擅长。这里简单说下主要的结论吧

  1. Imitation
    这里论文分别通过过拟合程度,以及student和teacher的预测一致性来衡量大模型是否无脑拟合了teacher模型。并提出了合适的正则项,以及early stopping机制可以降低模仿,提高泛化

  2. Sailency
    论文提出当强模型本身通过预训练对该任务已经有很好的任务学习(表征)的情况下,泛化会更好。这里个人感觉有些像DAPT,TAPT(domain/taskadaptive pretraining)的思路,不熟悉的同学看这里
    预训练不要停!Continue Pretraining
    。从文本表征空间分布的角度来说,就是当模型对该任务文本所在空间分布本身表征更加高维
    线性可分,边界更加清晰平滑
    时,模型更容易泛化到该任务上。

想看更全的大模型相关论文梳理·微调及预训练数据和框架·AIGC应用,移步Github >>
DecryPrompt

本文介绍如何实现进销存管理系统的基础数据模块,基础数据模块包括商品信息、供应商管理和客户管理3个菜单页面。供应商和客户字段相同,因此可共用一个页面组件类。

1. 配置模块

运行项目,在【系统管理-模块管理】中添加商品信息、供应商管理、客户管理3个模块菜单,模块基本信息、模型、页面、表单设置之前有视频教程,这里不再细说了。

2. 实体类


JxcLite
项目
Entities
文件夹下面添加
JxGoods.cs

JxPartner.cs
两个实体类文件,实体类代码可以直接复制模块管理中由模型设置生成的代码。文章中只简单描述一下实体类的定义,具体代码参见开源,代码定义如下:

namespace JxcLite.Entities;

/// <summary>
/// 商品信息类。
/// </summary>
public class JxGoods : EntityBase { }

/// <summary>
/// 商业伙伴信息类。
/// </summary>
public class JxPartner : EntityBase { }

3. 建表脚本

最理想的情况是:在系统安装时,通过实体类和数据库类型自动生成建表脚本创建实体数据库表。这里还是用传统手动方式执行建表脚本,在
JxcLite.Web
项目
Resources
文件夹下添加
Tables.sql
资源文件,复制粘贴由【模块管理-模型设置】中生成的建表脚本。文章中只简单描述一下建表脚本,具体脚本参见开源,内容如下:

CREATE TABLE [JxGoods] (
    [Id]         varchar(50)      NOT NULL PRIMARY KEY,
    ...
    [Files]      nvarchar(500)    NULL
);

CREATE TABLE [JxPartner] (
    [Id]         varchar(50)      NOT NULL PRIMARY KEY,
    ...
    [Note]       ntext            NULL,
    [Files]      nvarchar(500)    NULL
);

4. 服务接口


JxcLite
项目
Services
文件夹下面添加基础数据模块服务接口类,文件名定义为
IBaseDataService.cs
,该接口定义前后端交互的Api访问方法,包括分页查询、批量删除实体、保存实体。具体方法定义如下:

namespace JxcLite.Services;

public interface IBaseDataService : IService
{
    //分页查询商品信息
    Task<PagingResult<JxGoods>> QueryGoodsesAsync(PagingCriteria criteria);
    //批量删除商品信息
    Task<Result> DeleteGoodsesAsync(List<JxGoods> models);
    //保存商品信息
    Task<Result> SaveGoodsAsync(UploadInfo<JxGoods> info);

    //分页查询供应商和客户信息
    Task<PagingResult<JxPartner>> QueryPartnersAsync(PagingCriteria criteria);
    //批量删除供应商和客户信息
    Task<Result> DeletePartnersAsync(List<JxPartner> models);
    //保存供应商和客户信息
    Task<Result> SavePartnerAsync(UploadInfo<JxPartner> info);
}

5. 服务实现


JxcLite.Web
项目
Services
文件夹下面添加基础数据模块服务接口的实现类,文件名定义为
BaseDataService.cs
,文章中只简单描述一下实现类的定义和继承,具体实现参见开源,定义如下:

namespace JxcLite.Web.Services;

class BaseDataService(Context context) : ServiceBase(context), IBaseDataService
{
    public Task<PagingResult<JxGoods>> QueryGoodsesAsync(PagingCriteria criteria) { }
    public Task<Result> DeleteGoodsesAsync(List<JxGoods> models) { }
    public Task<Result> SaveGoodsAsync(UploadInfo<JxGoods> info) { }

    public Task<PagingResult<JxPartner>> QueryPartnersAsync(PagingCriteria criteria) { }
    public Task<Result> DeletePartnersAsync(List<JxPartner> models) { }
    public Task<Result> SavePartnerAsync(UploadInfo<JxPartner> info) { }
}

双击打开
JxcLite.Web
项目中的
AppWeb.cs
文件,在
AddJxcLiteCore
方法中注册服务类,前端组件可以通过依赖注入工厂创建服务的实例。代码如下:

public static class AppWeb
{
    public static void AddJxcLiteCore(this IServiceCollection services)
    {
        services.AddScoped<IBaseDataService, BaseDataService>();
    }
}

6. 数据依赖


JxcLite.Web
项目
Repositories
文件夹下面添加基础数据模块数据依赖类,文件名定义为
BaseDataRepository.cs
,文章中只简单描述一下依赖类的定义,具体实现参见开源,定义如下:

namespace JxcLite.Web.Repositories;

class BaseDataRepository
{
    internal static Task<PagingResult<JxGoods>> QueryGoodsesAsync(Database db, PagingCriteria criteria) { }

    internal static async Task<bool> ExistsGoodsCodeAsync(Database db, JxGoods model) { }

    internal static Task<PagingResult<JxPartner>> QueryPartnersAsync(Database db, PagingCriteria criteria) { }

    internal static async Task<bool> ExistsPartnerNameAsync(Database db, JxPartner model) { }
}

7. 数据导入类


JxcLite.Web
项目
Imports
文件夹下面添加商品信息、供应商和客户的导入类,文件名定义为
JxGoodsImport.cs

JxPartnerImport.cs

导入类名称命名规范是:实体类名+Import,导入框架自动根据名称识别
,文章中只简单描述一下导入类的定义,具体实现参见开源,定义如下:

namespace JxcLite.Web.Imports;

class JxGoodsImport(ImportContext context) : ImportBase<JxGoods>(context)
{
    //初始化导入字段,自动生成导入规范Excel文件
    public override void InitColumns() { }
    //执行导入文件
    public override async Task<Result> ExecuteAsync(SysFile file) { }
}

class JxPartnerImport(ImportContext context) : ImportBase<JxPartner>(context)
{
    public override void InitColumns() { }
    public override async Task<Result> ExecuteAsync(SysFile file) { }
}

8. 前端页面


JxcLite.Client
项目
Pages\BaseData
文件夹下面添加商品信息和商业伙伴页面类,文件名定义为
GoodsList.cs

PartnerList.cs
,这3个模块的功能和页面非常简单,只有单表的增删改查导功能,表单页面直接通过在线表单进行配置。列表页面继承
BaseTablePage
类,由于该框架类封装了列表页面常用的增删改查导功能,因此具体的功能列表页面代码显得格外简单,只需要定义各操作的服务调用方法即可,具体的完整代码如下:

  • 商品信息页面
namespace JxcLite.Client.Pages.BaseData;

[StreamRendering]
[Route("/bds/goods")]
public class GoodsList : BaseTablePage<JxGoods>
{
    private IBaseDataService Service;

    protected override async Task OnPageInitAsync()
    {
        await base.OnPageInitAsync();
        Service = await CreateServiceAsync<IBaseDataService>();
        Table.OnQuery = Service.QueryGoodsesAsync;
    }

    public void New() => Table.NewForm(Service.SaveGoodsAsync, new JxGoods());
    public void DeleteM() => Table.DeleteM(Service.DeleteGoodsesAsync);
    public void Edit(JxGoods row) => Table.EditForm(Service.SaveGoodsAsync, row);
    public void Delete(JxGoods row) => Table.Delete(Service.DeleteGoodsesAsync, row);
    public void Import() => ShowImportForm();
    public async void Export() => await ExportDataAsync();
}
  • 供应商和客户页面
[StreamRendering]
[Route("/bds/suppliers")]
public class SupplierList : PartnerList
{
    protected override string Type => PartnerType.Supplier;
}

[StreamRendering]
[Route("/bds/customers")]
public class CustomerList : PartnerList
{
    protected override string Type => PartnerType.Customer;
}

public class PartnerList : BaseTablePage<JxPartner>
{
    private IBaseDataService Service;

    //商业伙伴类型虚拟属性,供应商和客户页面覆写。
    protected virtual string Type { get; }

    protected override async Task OnPageInitAsync()
    {
        await base.OnPageInitAsync();
        Service = await CreateServiceAsync<IBaseDataService>();
        Table.OnQuery = QueryPartnersAsync;
    }

    public void New() => Table.NewForm(Service.SavePartnerAsync, new JxPartner { Type = Type });
    public void DeleteM() => Table.DeleteM(Service.DeletePartnersAsync);
    public void Edit(JxPartner row) => Table.EditForm(Service.SavePartnerAsync, row);
    public void Delete(JxPartner row) => Table.Delete(Service.DeletePartnersAsync, row);
    public void Import() => ShowImportForm();
    public async void Export() => await ExportDataAsync();

    private Task<PagingResult<JxPartner>> QueryPartnersAsync(PagingCriteria criteria)
    {
        criteria.SetQuery(nameof(JxPartner.Type), QueryType.Equal, Type);
        return Service.QueryPartnersAsync(criteria);
    }
}

在软件开发的世界里,注释是代码的伴侣,它们帮助我们记录思路,解释复杂的逻辑,以及为后来者提供指引。然而,注释的艺术在于找到恰当的平衡——既不过于冗余,也不过于吝啬。本文将探讨如何优雅地写出恰到好处的注释。

注释有啥用

首先,我们需要认识到注释的价值。好的注释可以:

  • 提高代码的可读性:让其他开发者或未来的你快速理解代码段的功能和目的。
  • 促进团队协作:在团队项目中,清晰的注释可以减少沟通成本。
  • 加快调试过程:当出现问题时,注释可以帮助快速定位问题所在。

所以,必须写注释。当阅读源代码时,没有注释会使大脑负担加重,就像你去查看Spring的源代码一样,几乎没有注释。你能看到的只有在抛出异常时提供的少量信息。因此,并不是大多数程序员不理解Spring,而是有时候它并不打算让人轻易理解。

image

注释原则

要写出优雅的注释,可以遵循以下几个原则:

  • 相关性:只对重要的逻辑和决策进行注释,避免对显而易见的代码进行注释。
  • 简洁性:注释应简洁明了,避免冗长和啰嗦。
  • 清晰性:确保注释清晰表达其意图,避免模糊不清的描述。
  • 更新性:随着代码的更新,及时更新相关的注释,避免产生误导。

以下就是一些奇葩注释反例,值得深思:

/*

*你可能觉得自己看懂下面的代码了,

*然而你并没有,相信我。

*糊弄过去算了,不然你会好多个晚上睡不着觉,

*嘴里骂着这段注释,觉得自己很聪明,

*真能“优化”下面的代码。

*现在关上文件,去玩点别的吧。

*/
//我也不确定我们到底需不需要这个,但是删了又特害怕。
//他们让我写的,非本人自愿。

实践技巧

在实际编码中,以下是一些有用的注释技巧:

  • 函数和方法注释:为每个函数和方法提供简短的描述,包括其参数、返回值和可能抛出的异常。
  • 复杂的逻辑块:对于复杂的逻辑,提供简短的解释,帮助理解其目的和工作原理。
  • TODO注释:使用TODO来标记需要进一步处理或改进的地方。
  • 假设和决策:对于基于特定假设或决策的代码,注释这些假设和决策的原因。

例如,现在有许多AI编码工具可以帮助我们编写代码,这些工具基本上能显著减少我们的打字时间。利用节省下来的时间,我们可以更专注于优化注释内容。这不仅有助于提升我们自己对代码的理解,也能极大地帮助其他人更快地掌握和维护代码。

image

总结

优雅的注释是一种平衡艺术,它要求我们在不牺牲代码清晰度的前提下,避免过度注释。通过遵循上述原则和技巧,我们可以写出既有助于自己,也有助于他人的注释,从而提升代码的整体质量和可维护性。

记住,注释的目的是为了沟通,无论是与未来的自己,还是与现在的团队成员。找到那个黄金平衡点,让你的代码因优雅的注释而更加生动。

本篇为译文

原文地址
https://avaloniaui.net/blog/avalonia-11-1-a-quantum-leap-in-cross-platform-ui-development

github地址 https://github.com/AvaloniaUI/Avalonia


史蒂文·柯克
发布于 7 月 22 日

我们很高兴地宣布发布 Avalonia 11.1,这是对我们喜爱的跨平台 UI 框架的大规模更新。虽然从技术上讲是一个点发布,但改进的数量和影响使其感觉更像是一次重大升级。此版本代表了我们的专业团队和社区贡献者无数个小时的辛勤工作,带来了许多新功能、性能增强和全面改进。让我们深入探讨此版本中最令人兴奋的方面,这些方面将使您能够创建更令人印象深刻的跨平台应用程序。

增强的跨平台支持

Avalonia 一直以支持最广泛的平台而自豪,11.1 版本将其提升到了新的高度:

电视支持:Avalonia 11.1 支持 Samsung Tizen 和 Apple TV 平台,显着拓宽了其在智能电视生态系统中的影响力。这一扩展为希望将其应用程序带到大屏幕上的开发人员开辟了令人兴奋的新可能性。借助 Samsung Tizen 支持,您现在可以瞄准三星智能电视和其他基于 Tizen 的设备,从而进入不断增长的细分市场。同时,添加 Apple TV 支持可让您在 Apple 生态系统中创建令人惊叹的 UI 体验。对于那些希望将应用程序扩展到客厅或创建独特的基于电视的体验的开发人员来说,这些新功能尤其令人兴奋。

浏览器改进:Avalonia 的浏览器支持得到了显着增强。实施了新的软件渲染器,提供了更好的性能和跨浏览器的兼容性。此外,该框架现在允许多个 AvaloniaView 实例,从而支持更复杂和动态的 Web 应用程序。这些改进极大地增强了在 Web 浏览器中运行的 Avalonia 应用程序的灵活性和性能,使其成为基于 Web 的项目的更可行的选择。

Android 和 iOS 增强功能:对 Android 和 iOS 后端进行了各种改进,包括输入处理、键盘交互和一般稳定性增强的修复。这些改进确保了移动平台上的开发人员和最终用户获得更流畅的体验。

性能优化

性能是我们最重要的功能之一,Avalonia 11.1 在这方面提供了重大改进:

Vulkan 后端:主要新增功能是新的 Vulkan 渲染后端。这种现代、低开销的图形 API 可以显着提高性能,尤其是在原生支持它的平台上。 Vulkan 提供对 GPU 的更直接控制,从而更好地利用图形硬件。它可以提高渲染性能并可能降低功耗,这对于移动设备和高性能桌面应用程序尤其有利。

渲染优化:对渲染管道进行了大量优化,包括对脏矩形处理、变换操作和一般绘图性能的改进。这些增强功能可带来更流畅的动画、更快的 UI 更新以及对应用程序的整体响应更快的感觉。该团队在减少不必要的重绘和优化渲染过程方面投入了大量精力,这在具有许多元素的复杂 UI 中应该特别明显。

资源管理:更好地管理资源(包括字体和 XAML 资源)应该会减少内存使用量并缩短应用程序启动时间。该框架现在采用更高效的缓存机制和延迟加载策略,确保仅在需要时加载资源。这不仅可以缩短初始加载时间,还有助于减少应用程序的总体内存占用。

用户界面和用户体验改进

Avalonia 11.1 引入了多项功能,将帮助开发人员创建更加精美和用户友好的界面:

HyperlinkBut​​ton 控件:添加了新的 HyperlinkBut​​ton 控件,使您可以更轻松地在 UI 中实现可点击链接。该控件具有适合超链接的​​内置样式和行为,可以节省开发人员的时间并确保应用程序之间的一致性。

改进的工具提示系统:工具提示系统已通过新功能(如工具提示链接和新的 BetweenShowDelay 属性)进行了彻底修改。工具提示链接允许创建更复杂的信息层次结构,其中从一个元素移动到相关元素可以使相关信息保持可见。 BetweenShowDelay 属性可以对工具提示时间进行细粒度控制,从而允许更复杂的工具提示行为。

增强的 ScrollViewer:ScrollViewer 的改进包括更好地处理鼠标滚轮的滚动捕捉,从而提供更流畅的滚动体验。增强的 ScrollViewer 现在提供更自然的滚动感觉,适应不同的输入方法,从触摸板到传统的鼠标滚轮。这会给您的应用程序带来更加精致的感觉,尤其是那些具有长滚动内容的应用程序。

开发人员生产力功能

Avalonia 11.1 包含多项旨在让开发人员的生活更轻松、更高效的功能:

改进的开发工具:内置的开发人员工具已得到增强,具有焦点跟随器和固定属性的功能等功能。焦点跟随器允许开发人员轻松实时跟踪哪个元素具有焦点,这对于调试复杂的输入场景非常有用。在属性检查器中固定属性的功能使您在与应用程序交互时可以更轻松地监视特定值。

XAML 改进:XAML 系统进行了许多改进,包括更好地处理泛型、更高效的编译绑定以及针对 XAML 相关问题的增强诊断。改进的泛型支持允许更灵活和可重用的组件定义。编译的绑定现在生成更优化的代码,从而实现更好的运行时性能。增强的诊断功能提供更清晰的错误消息和警告,帮助开发人员在开发周期的早期发现并修复问题。

增强的绑定系统:数据绑定系统得到了改进,改进了 MultiBinding、更好地处理样式设置器中的 ICommand 绑定以及更具反应性的 PropertyChanged 事件。 MultiBinding 现在可以更优雅地处理空值并提供更好的性能。样式设置器中 ICommand 绑定的改进使得以更具声明性的方式创建交互式 UI 元素变得更加容易。更具反应性的 PropertyChanged 事件可确保您的 UI 更可靠地与数据模型保持同步。

新转换器类型:引入具有参数支持的 FuncValueConverter,为数据转换场景提供了更大的灵活性。这种新的转换器类型允许更简洁和可读的绑定表达式,特别是对于不需要完整转换器类的简单转换。它对于快速原型设计或简单的一次性转换特别有用。

例如,Avalonia 现在有 ObjectConverters.Equal,其实现如下:

图形和动画增强

此版本中图形功能得到了扩展。

新的像素格式:添加了对 Rgb32 和 Bgr32 像素格式的支持,为图像处理和操作提供了更多选项。这些新格式可以更有效地处理某些类型的图像,并可以提高图像密集型应用程序的性能。它们还提供与某些外部图像库和文件格式更好的兼容性。

改进的画笔处理:TileBrush 和 DrawingBrush 的增强功能为创建复杂的图形效果提供了更强大、更灵活的选项。 TileBrush 现在可以更好地控制平铺行为,包括改进对边缘情况的处理。 DrawingBrush 已针对性能进行了优化,现在支持更复杂的绘图操作。这些改进允许用更少的代码和更好的性能创建更复杂的视觉效果。

动画改进:动画系统得到了改进,包括修复 Animator 类以处理小于零的进度值。这确保了动画更流畅、更可预测,特别是对于复杂序列或处理动态变化的值时。此外,动画的整体性能也得到了改进,允许更复杂的动画而不影响应用程序的响应能力。

可访问性和国际化

Avalonia 不断改进对创建无障碍应用程序的支持:

改进的屏幕阅读器支持:包括 DataGrid 在内的各种控件的自动化属性得到了增强,从而提高了屏幕阅读器兼容性。这包括更好的标签、更具描述性的状态更改以及改进的导航支持。这些增强功能使开发人员可以更轻松地创建可供视力障碍人士使用的应用程序,而无需进行大量额外编码。

更好的输入法编辑器 (IME) 支持:IME 处理得到了改进,特别有利于需要复杂输入法的语言的用户。这包括更好地处理合成事件、改进的光标定位以及更可靠的文本插入。这些增强功能使 Avalonia 应用程序对于中文、日语和韩语等语言的用户来说更易于使用。

增强的本地化支持:添加了新的 API,可以更轻松地本地化内置控件和消息。这包括改进用于管理本地化字符串的资源系统以及更好地支持从右到左的语言。这些功能简化了面向全球受众的应用程序的创建。

针对移动设备的增强功能

认识到移动开发日益增长的重要性,Avalonia 11.1 包括多项针对移动设备的改进:

文本选择手柄:在 TextBox 控件中实现触摸输入的文本选择手柄,在移动设备上提供更原生的感觉。此功能模仿了用户在移动平台上期望的行为,使文本选择和编辑更加直观和用户友好。

安全区域处理:改进了移动设备上安全区域的处理,确保您的 UI 正确适应不同的设备外形尺寸和方向。这对于带有凹口或圆角的设备尤其重要,可确保您的 UI 内容始终可见且不会被设备功能遮挡。

移动手势识别:手势识别的增强功能(尤其是针对触摸设备)可在移动平台上提供更灵敏、更直观的用户体验。这包括对捏合缩放、滑动手势和多点触控交互的改进。这些增强功能使开发人员能够创建更自然的移动界面,而无需实现自定义手势识别器。


新的窗口功能

对窗口管理进行了多项改进,增强了开发人员对其应用程序演示的灵活性和控制力:

多显示器 DPI 缩放:更好地处理跨多个显示器的 DPI 缩放,确保您的应用程序在所有显示器上看起来清晰且尺寸正确。这在当今多样化的计算环境中尤其重要,用户通常拥有多个具有不同缩放系数的显示器。 Avalonia 现在可以更优雅地处理这些场景,确保所有屏幕上的外观一致。

窗口 Z 顺序 API:用于获取窗口 Z 顺序的新 API 使开发人员能够更好地控制窗口堆叠和管理。这对于管理多个窗口或创建自定义窗口管理行为的应用程序特别有用。它允许更复杂的 MDI 样式界面或自定义窗口管理解决方案。

改进的窗口大小调整和定位:窗口大小调整、定位和状态管理的各种增强功能提供了跨平台更可靠和一致的行为。这包括更好地处理最大化和最小化状态、更准确的初始定位以及改进的调整大小行为。这些改进可确保您的应用程序在不同操作系统和窗口管理器中的行为可预测。


文件系统集成

Avalonia 11.1 改进了与本机文件系统的集成,使创建与主机操作系统无缝协作的应用程序变得更加容易:

增强的文件对话框:文件选择器对话框得到了改进,更好地支持文件类型过滤和初始目录选择。这些增强功能可以更轻松地创建符合平台期望的直观文件选择体验。改进的过滤选项允许更精细地控制用户可以选择的文件类型。

文件激活支持:用于处理文件激活事件的新 API 使您可以更轻松地在应用程序中实现文件关联功能。这允许您的 Avalonia 应用程序在用户从操作系统打开文件时做出响应,从而提供更加集成的体验。它对于以文档为中心的应用程序或处理特定文件类型的工具特别有用。

构建和部署改进

为了改进构建和部署过程,进行了一些更改,使创建和分发 Avalonia 应用程序变得更加容易:

NativeAOT 支持:改进了对 NativeAOT 编译的支持,确保 Avalonia 应用程序可以充分利用这种性能增强技术。 NativeAOT 通过提前将应用程序编译为本机代码,可以缩短启动时间并减少内存使用量。这对于启动性能至关重要的桌面应用程序尤其有利。

简化资源处理:对程序集资源处理方式的更改应该会导致应用程序尺寸更小和加载时间更快。优化了资源系统,减少重复,提高加载效率。这可以导致具有许多嵌入式资源的大型应用程序的大小显着减小。

增强的 XAML 编译:XAML 编译过程的改进提供了更好的性能和更可靠的错误报告。编译器现在可以生成更优化的代码,并在检测到问题时提供更清晰的错误消息。这有助于在开发过程中尽早发现问题并提高运行时性能。

这些构建和部署增强应该会带来更小、更快的应用程序。


结论

Avalonia 11.1 代表了该框架向前迈出的重要一步。改进的广度和深度体现了我们团队致力于提供顶级跨平台 UI 开发体验的承诺。从扩展的平台支持和性能优化到增强的开发人员工具和改进的可访问性,此版本几乎涉及框架的每个方面。

我们鼓励所有 Avalonia 开发人员升级到这个新版本,并探索它提供的丰富新功能和改进。您的反馈和贡献对于塑造 Avalonia 的未来非常宝贵,因此请随时与社区分享您的经验和建议。

我们要向核心团队和社区的所有贡献者致以衷心的感谢,是他们使此版本成为可能。你们的辛勤工作、创造力和奉献精神使 Avalonia 成为如此强大且深受喜爱的框架。

祝您开发愉快,我们迫不及待地想看到您使用 Avalonia 11.1 创建的令人惊叹的应用程序!

本文介绍基于
R
语言中的
readxl
包与
ggplot2
包,读取
Excel
表格文件数据,并绘制具有
多个系列

柱状图

条形图
的方法。

首先,我们配置一下所需用到的
R
语言
readxl
包与
ggplot2
包;其中,
readxl
包是用来读取
Excel
表格文件数据的,而
ggplot2
包则是用以绘制柱状图的。包的下载方法也非常简单,以
readxl
包为例,我们输入如下的代码即可。

install.packages("readxl")

输入代码后,按下
回车
键,运行代码;如下图所示。

image


readxl
包下载完成后,通过同样的方法配置
ggplot2
包。

install.packages("ggplot2")

此外,在用代码进行数据分析、可视化时,有时需要对数据加以
长数据

宽数据
的转换(具体什么意思在后文有介绍),这里需要用到另一个
R
语言包
reshape2
,我们也就在此将其一并配置好。

install.packages("reshape2")

接下来,我们即可开始代码的撰写。首先,我们将需要用到的包导入。

library(readxl)
library(ggplot2)
library(reshape2)

随后,我们进行
Excel
表格文件数据的读取;这里我们就通过
readxl
包中的
read_excel()
函数来实现表格数据的读取。其中,函数的第一个参数表示待读取的
Excel
表格文件路径与名称,第二个参数则表示这些数据具体在哪一个
Sheet
中;由于我这里需要的数据存放在
Excel
表格文件的第
2

Sheet
中,因此就选择
sheet = 2
即可。

xlsx_file <- read_excel(r"(E:\02_Project\01_Chlorophyll\ClimateZone\Split\Result\Result.xlsx)", sheet = 2)

其中,原本在表格文件中我的数据如下所示。

通过上述代码,我们即可将数据读入
R
语言中;其具体格式如下图所示。可以看到,读入后的数据是一个
tibble
类别的变量,
tibble

Data Frame
格式数据的一种改进,我们在这里可以就将其视作
Data Frame
格式数据加以后续处理。

此外,如果大家是使用
RStudio
软件进行代码的撰写,还可以双击这一变量,更直观地查看读入后的数据具体是什么样子的,如下图所示。

接下来,我们需要对数据加以长、宽转换。首先,简单来说,
宽数据
就是如
上图
所示的数据,而
长数据
则是如
下图
所示的数据;其中,我们在获取、记录原始数据时,往往获取的是
宽数据
,因为这一类数据具有更加直观、更易记录的特点;而在用数据分析软件或代码对数据加以深入处理或可视化操作时,往往系统需要的是
长数据
。因此,我们这里需要对
宽数据

长数据
加以转换;这一转换可以通过
melt()
函数来实现,具体的代码如下所示。

xlsx_data <- melt(xlsx_file, id.var = "...1")

其中,
melt()
函数的第一个参数表示需要进行转换的变量,第二个参数则是
ID变量
,一般情况下就是
表述数据序号
的第一列数据;我这里由于原本
Excel
的数据中就没有
表示序号
的那一列数据,因此就选择了原有数据的第一列作为
ID变量
。执行上述代码后,我们得到的长数据如下图所示。

此外,
melt()
函数在运行时,还可以指定数据转换后的列名。如以下代码就表示,我们希望将转换后表示变量的列的名称设置为
Factor
,表示观测值的列的名称设置为
q

xlsx_data <- melt(xlsx_file, id.var = "...1", variable.name = "Factor", value.name = "q")

执行上述代码,得到的长数据如下图所示。

当然,这里需要提一句,关于
宽数据

长数据
的转换,涉及到很多内容;如果大家有需要,可以查看
melt()
函数的官方帮助文档。

完成数据格式转换后,我们即可开始绘图。这里我们就直接通过
ggplot2
包的
ggplot()
函数,对柱状图加以绘制即可;具体代码如下所示。

ggplot(data = xlsx_data, mapping = aes(x = Factor, y = q, fill = ...1)) + geom_bar(stat = "identity", position = "dodge")

其中,
ggplot()
函数的第一个参数
data
表示需要参与绘图的数据,第二个参数
mapping
表示我们需要用哪一列数据作为
X
轴,哪一列作为
Y
轴;同时,其内部的
fill
参数表示我们需要将柱状图分为
多个系列
(如果大家的柱状图只有
1
个系列,那么就不需要
fill
这个参数了),其后指定的变量就表示我们需要基于这一变量对
数据的系列
加以区分。接下来,加号后面的
geom_bar
参数,是我们绘制多序列柱状图所需要设定的,其中
position
参数设置为
"dodge"
就表示我们希望将不同的系列平行放置(如果不设置
position
参数,那么不同系列的柱子就会垂直堆积,有点类似
堆积柱状图
)。

执行上述代码,得到如下所示的结果。

此外,如果大家希望柱状图是横向伸展的,就在最后增添
+ coord_flip()
代码即可。

ggplot(data = xlsx_data, mapping = aes(x = Factor, y = q, fill = ...1)) + geom_bar(stat = "identity", position = "dodge") + coord_flip()

执行上述代码,得到如下所示的结果。

在这里,我们仅仅是对
ggplot()
函数做了一个初步的介绍;关于其更深入的了解,大家直接查看其官方帮助文档即可。

至此,大功告成。