2024年9月

全局弹窗相对于自定义弹窗有以下优点:

  • 封装更彻底,一行代码就能调用
  • 跟组件耦合度低,只需要传入组件的UIContext对象,不需要跟自定义弹窗一样需要在组件内部实例化CustomDialogController对象

全局弹窗是鸿蒙在API 12增加的,PromptAction对象增加了openCustomDialog方法。

代码实现

首先创建一个接口,用于参数的传递,弹窗内按钮的点击

interface GlobalDialogParam {
    content:string; //弹窗显示内容
    onConfirm: () => void //确认按钮的回调函数
    onCancel: () => void// 取消按钮的回调函数
}

自定义弹窗内容,使用@Builder装饰器表明该函数将返回一个 UI 组件树,弹窗内容根据您的需求自己实现。本例中就显示一个简单的对话框。

@Builder function buildGlobalDialogComponent(param: GlobalDialogParam){
    Column() {
        Text(param.content).fontSize(17).fontColor("#181818")
        .lineHeight(24).margin({
            bottom:29,top:29,left:31,right:31
        })

        Divider().color("#D8D8D8").height(0.5)
        RowSplit() {
            Text("取消").fontSize(17).fontColor("#181818")
            .fontWeight(FontWeight.Bold).onClick(event=>{
                param.onCancel();
            }).textAlign(TextAlign.Center).padding({
                top:15,bottom:15
            }).width('50%')

            Text("确定").fontSize(17).fontColor($r('app.color.mainColor'))
            .fontWeight(FontWeight.Bold).onClick(event=>{
                param.onConfirm();
            }).textAlign(TextAlign.Center).padding({
                top:15,bottom:15
            }).width('50%')
        }
    }.backgroundColor($r('app.color.white')).width('80%').borderRadius(12)
}

在GlobalDialog类中增加两个静态方法,用来显示弹窗跟关闭弹窗,关键代码都增加来注释,这里就不过多解释了

export class GlobalDialog {
    static contentNode:ComponentContent<GlobalDialogParam>;

    //显示弹窗
    static show(context: UIContext,dialogParam: GlobalDialogParam){
        //ComponentContent对象有三个参数
        //参数1:UI 上下文
        //参数2:使用 wrapBuilder 包装 buildGlobalDialogComponent 函数,这个函数用于构建对话框的实际内容
        //参数3:传递给对话框的参数,包含内容文本和按钮的回调函数
        GlobalDialog.contentNode = new ComponentContent(context, wrapBuilder(buildGlobalDialogComponent), dialogParam);

        const promptAction = context.getPromptAction()//通过 context 获取 promptAction,用于操作对话框显示

        //显示弹窗
        promptAction.openCustomDialog(GlobalDialog.contentNode,{
            alignment: DialogAlignment.Center,//对话框在屏幕中央显示
            autoCancel: false,//点击弹窗外区域是否取消弹窗
        });
    }

    //关闭弹窗
    static close(context: UIContext){
        const promptAction = context.getPromptAction()
        promptAction.closeCustomDialog(GlobalDialog.contentNode)
    }
}

通过以上三个步骤,全局弹窗的代码就封装好了,接下来在组件中如何调用呢?其实代码很简单,调用GlobalDialog.show方法显示弹窗,在确定跟取消按钮的回调函数中调用GlobalDialog.close取消弹窗。

GlobalDialog.show(this.getUIContext(),{
    content:"您确定要删除这条记录吗?",
    onConfirm:()=>{
        GlobalDialog.close(this.getUIContext())//关闭弹窗
        AppUtil.showToast("确定按钮点击");
    },
    onCancel:()=>{
        GlobalDialog.close(this.getUIContext())//关闭弹窗
        AppUtil.showToast("取消按钮点击");
    }
})

效果图:

延伸阅读,@Builder 装饰器

在鸿蒙的 ArkUI 开发中,@Builder 装饰器是一种用于简化组件构建的标记,它通常用于函数上,指示该函数返回一个 UI 组件。

@Builder 装饰器的作用:

  1. 生成UI组件:
    @Builder 装饰器标记的函数主要用于构建 UI 组件。它将函数体内定义的 UI 布局和组件树返回给调用方,以便在应用程序中使用这些组件。
  2. 提高代码可读性和模块化:
    通过使用 @Builder,可以把复杂的 UI 构建逻辑封装到一个函数中,使得代码更简洁和模块化,便于复用。例如,常见的对话框、弹窗、复杂组件可以通过这样的函数构建,并在不同的地方调用。
  3. 函数式UI构建:
    鸿蒙的 ArkUI 是声明式 UI 框架,@Builder 提供了一种函数式的 UI 组件创建方式。开发者可以通过定义函数和内部组件来构建界面,并使用该函数返回的组件进行显示。

源码下载

全局弹窗的代码都提交到github上了,这个库我会一直维护,这个一个鸿蒙API使用案例的工具库,后续会陆续增加功能以及维护。

https://github.com/ansen666/harmony_tools


扫一扫
关注我的公众号

在 Web 开发中,我们经常需要区分用户是否通过刷新操作重新加载了页面。这一操作可能是由用户手动刷新(如按下 F5 键或点击浏览器刷新按钮)或通过浏览器自动重新加载。判断页面是否刷新有助于开发者优化用户体验,例如在使用 vue 的时候需要进行权限控制,就需要判断在刷新后根据登录者的权限去添加对应的路由。

本文将详细解析几种常见的判断页面是否刷新的技术方案,并探讨各自的适用场景、优缺点以及浏览器的兼容性。

1. 使用
window.name

window.name
是一个持久的窗口属性,它的值在页面刷新、甚至通过标签页导航到其他页面时也会保留,因此可以利用它来判断页面是否是通过刷新重新加载。

代码示例

window.onload = function() {
  if (window.name === 'isRefreshed') {
    console.log('页面被刷新');
  } else {
    console.log('首次加载页面');
    window.name = 'isRefreshed';
  }
};

工作原理

  • 首次加载时,
    window.name
    是空字符串,通过设置它为
    'isRefreshed'
    来标记状态。
  • 刷新页面后,
    window.name
    仍保持为
    'isRefreshed'
    ,因此可以判断页面是通过刷新加载的。

优点

  • 简单易用
    :不依赖外部存储机制或服务器端逻辑。
  • 跨页面持久性
    :在页面间导航时,
    window.name
    的值依然保持,适合跨页面场景。

缺点

  • 安全性问题

    window.name
    的值在不同页面间共享,可能被其他页面读取。
  • 手动清理
    :在某些场景下可能需要手动清除
    window.name
    ,例如页面关闭时。

兼容性

window.name
是一个非常老的 Web API,几乎在所有浏览器中都有广泛的支持,包括:

2. 使用
sessionStorage

sessionStorage
是 Web 存储 API 的一部分,它为每个标签页维护独立的存储空间,并且其数据在标签页关闭后会被清空。我们可以利用
sessionStorage
来判断页面是否被刷新:

window.onload = function() {
  if (sessionStorage.getItem('isRefreshed')) {
    console.log('页面被刷新');
  } else {
    console.log('首次加载页面');
  }
  sessionStorage.setItem('isRefreshed', true);
};

工作原理

  • 当页面首次加载时,
    sessionStorage
    中没有
    isRefreshed
    条目,因此可以判断这是首次加载。
  • 通过设置
    sessionStorage.setItem('isRefreshed', true);
    ,标记页面已加载。
  • 当页面刷新后,
    sessionStorage
    中的
    isRefreshed
    条目依然存在,因此可以检测到页面的刷新操作。

优点

  • 简单且不依赖服务器端逻辑。
  • 只对当前标签页有效,适合单个页面或 SPA(单页应用)场景。

缺点

  • 关闭标签页或浏览器窗口后,
    sessionStorage
    会被清空,无法保存状态。

兼容性

sessionStorage
是广泛支持的 API,适用于以下浏览器:

3. 使用
performance.navigation
API

浏览器的
performance.navigation
API 提供了页面加载的详细信息,包括是否是通过刷新操作加载的页面。通过检查
performance.navigation.type
属性可以判断页面的加载方式。

window.onload = function() {
  if (performance.navigation.type === performance.navigation.TYPE_RELOAD) {
    console.log('页面被刷新');
  } else {
    console.log('首次加载页面');
  }
};

属性解释

  • performance.navigation.TYPE_RELOAD
    : 表示页面通过刷新加载。
  • 其他类型(如
    TYPE_NAVIGATE
    )表示正常导航。

优点

  • 直接提供了判断页面刷新与否的接口,较为精确。
  • 不需要手动存储状态。

缺点

  • 该 API 正在逐步弃用,未来的浏览器可能不会支持。
  • 不适合未来长期维护的项目,应考虑迁移到更新的 API,比如下文中的
    performance.getEntriesByType

兼容性

performance.navigation
API 在大多数浏览器中都被支持,但该 API 已逐步被弃用:

4. 使用
beforeunload
事件

beforeunload
事件在用户离开页面之前触发,无论是页面刷新、关闭还是导航到其他页面。在此事件中,我们可以设置一个标志位来判断用户是否通过刷新离开当前页面。

window.addEventListener('beforeunload', function() {
  localStorage.setItem('isRefreshed', 'true');
});

window.onload = function() {
  if (localStorage.getItem('isRefreshed') === 'true') {
    console.log('页面被刷新');
    localStorage.removeItem('isRefreshed');  // 刷新后清除标志位
  } else {
    console.log('首次加载页面');
  }
};

工作原理

  • 在页面卸载时(包括刷新),通过
    beforeunload
    事件设置一个标志位。
  • 页面重新加载时,根据该标志位判断页面是否通过刷新操作加载。

优点

  • 灵活,可以处理不同类型的页面离开操作。
  • localStorage
    的数据不会在页面关闭时清除,因此可以用于判断跨页面的刷新。

缺点

  • beforeunload
    事件在部分浏览器(尤其是移动端)可能表现不一致。
  • 如果用户清除了浏览器缓存或
    localStorage
    ,则无法正确判断。

兼容性

beforeunload
事件在大多数现代浏览器中都有广泛支持,但可能在一些移动端浏览器上表现不一致:

5. 使用
performance.getEntriesByType

performance.getEntriesByType("navigation")
是一个现代 Web 性能 API,用于获取页面导航的详细信息。通过这个方法,我们可以获取一个包含导航信息的对象,并通过检查该对象的
type
属性,判断页面是通过刷新加载还是其他方式进入的。

示例代码

window.onload = function() {
  const [navigationEntry] = performance.getEntriesByType('navigation');
  
  if (navigationEntry && navigationEntry.type === 'reload') {
    console.log('页面被刷新');
  } else {
    console.log('首次加载页面');
  }
};

工作原理

  • performance.getEntriesByType('navigation')
    返回一个
    PerformanceNavigationTiming
    对象数组,其中包含页面导航的详细信息。
  • 通过检查
    navigationEntry.type
    ,可以确定页面加载的类型:
    • type === 'reload'
      : 页面通过刷新加载。
    • type === 'navigate'
      : 页面通过正常导航进入。
    • type === 'back_forward'
      : 页面通过浏览器的前进或后退按钮加载。
    • type === 'prerender'
      : 页面通过预渲染加载(这个状态通常不常见)。

优点

  • 现代性

    performance.getEntriesByType
    是较新的 API,能够在现代浏览器中准确区分页面的导航方式。
  • 详细信息
    :除了判断页面刷新,还可以获取更多关于页面加载性能的数据,如 DNS 解析时间、请求时间等,有助于调优页面性能。
  • 无状态管理
    :无需依赖
    sessionStorage

    localStorage
    等外部状态,避免了状态同步问题。

缺点

  • 浏览器兼容性
    :虽然大多数现代浏览器支持此 API,但 Internet Explorer 不支持(现在已不是问题)。
  • 不适用于多次刷新
    :如果需要在用户进行多次刷新的情况下进行追踪,单次判断可能不足。

使用场景

performance.getEntriesByType
适合那些只需要快速判断页面是否是刷新加载的场景,并且同时有进一步性能优化需求的应用。对于现代 Web 开发,这是一个较为精确且无需额外存储或会话管理的解决方案。

监控页面加载性能示例

window.onload = function() {
  const [navigationEntry] = performance.getEntriesByType('navigation');

  if (navigationEntry) {
    console.log(`页面加载类型: ${navigationEntry.type}`);
    console.log(`页面加载时间: ${navigationEntry.loadEventEnd - navigationEntry.startTime} ms`);
  }
};

这种方式不仅能帮助判断页面加载类型,还能帮助开发者优化页面性能,提供更多性能数据来分析页面加载瓶颈。

兼容性

performance.getEntriesByType
是较新的 API,在现代浏览器中得到广泛支持,但较旧浏览器不支持:

总结

判断页面是否刷新是一个常见的需求,本文介绍了五种技术方案。每种方案都有其特定的适用场景和优缺点。总结如下:

方案 优点 缺点 浏览器兼容性
window.name 简单、易跨页面保持状态 安全性问题,需手动清理 适用于所有现代浏览器
sessionStorage 简单,不依赖复杂逻辑 关闭标签页时清空 支持现代浏览器及部分较旧浏览器
performance.navigation 直接提供页面刷新判断 API 正被弃用 广泛支持,但逐渐被废弃
performance.getEntriesByType 精确判断加载类型 较新,旧版浏览器不支持 仅支持现代浏览器
beforeunload 灵活,可处理多种离开页面的操作 部分浏览器不支持,尤其是在移动端 大多数现代浏览器支持

不同的方案各有优劣,开发者应根据应用的目标用户群体、性能需求和浏览器支持情况灵活选择。如果需要简单、跨页面的刷新判断,
window.name
是一个不错的选择;而在需要更精确、现代化的判断方式时,
performance.getEntriesByType
提供了更高的灵活性。

在许多实际应用中,相对于反映类别之间微妙差异的细粒度标签,我们更容易获取粗粒度标签。然而,现有方法无法利用粗标签以无监督的方式推断细粒度标签。为了填补这个空白,论文提出了
FALCON
,一种从粗粒度标记数据中无需细粒度级别的监督就能发现细粒度类别的方法。
FALCON
同时推断未知的细粒度类别和粗粒度类别之间的潜在关系。此外,
FALCON
是一种模块化方法,可以有效地从多个具有不同策略的数据集中学习。我们在八个图像分类任务和一个单细胞分类任务上评估了
FALCON

FALCON

tieredImageNet
数据集上超过最佳基线
22
%,实现了
600
多个细粒度类别。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Fine-grained Classes and How to Find Them

Introduction


机器学习在具有大量精确标记数据的领域表现出色。虽然粗粒度标签通常是丰富且易于获得的,但由于类别之间微妙的差异和少量具有区分性的特征,精细标签的精确注释却具有挑战性。因此,在许多领域,获取这种精细标签需要领域专业知识和繁琐的人工努力。例如,
B
细胞和
T
细胞可以很容易区分,但区分
CD4
+T细胞和
CD8
+T细胞等非常细粒度的细胞亚型则需要识别极少量的特定标记。为了自动化获得精细标签的繁琐工作,需要能够区分细粒度标签中微妙差异的机器学习方法。

先前的研究表明,粗粒度标签可用于更有效地学习细粒度类别。弱监督分类方法使用粗粒度标签作为一种弱监督形式,以提高细粒度分类性能。最近,出现了少样本学习方法。它们在一组粗粒度类别上进行训练,然后通过每个类别仅有的几个标记样本进行细粒度分类的适配。然而,所有这些方法都需要预设一组细粒度类别以及获取它们的一小部分样本。

在这项工作中,论文提出了一种名为
FALCON

Fine grAined Labels from COarse supervisioN
)的方法,它可以在一个粗标记的数据集中发现细粒度类别,并且无需任何监督。
FALCON
的关键发现是,细粒度预测可以通过结合粗粒度和细粒度类别之间的关系来恢复粗粒度预测。基于这个发现,
FALCON
开发了一个专门的优化过程,交替进行推断粗粒度和细粒度类别之间的未知关系和训练细粒度分类器。粗粒度和细粒度类别之间的关系是通过解决一个离散优化问题来推断的,而细粒度分类器则使用粗粒度监督和细粒度的伪标签进行训练。此外,
FALCON
可以无缝地适应和利用具有多个数据集的不兼容粗粒度类别,并以相同的细粒度级别重新标记。


FALCON
与其他备选基准方法在八个图像分类数据集以及生物领域的单细胞数据集上进行了比较。实验结果显示,
FALCON
在没有监督的情况下有效地发现了细粒度类别,并且在图像和单细胞数据上始终优于基线方法。例如,在包含
608
个细粒度类别的
tieredImageNet
数据集上,
FALCON
的性能比基准方法提高了
22%
。此外,当使用具有不同粗粒度类别的多个数据集进行训练时,
FALCON
能够有效地重用不同的注释策略来改进其性能。

Fine-grained Class Discovery


Problem setup


\(\mathcal{X}\)
为样本空间,
\(\mathcal{Y}_C\)
为包含
\(K_C\)
个粗粒度类别的集合。假设给定了一个粗粒度标记的数据集
\(\mathcal{D}=\{(\mathbf{x}^i,y_c^i)\}_{i=1}^N\)
,其中
\(\mathbf{x}^i\in\mathcal{X}\)

\(y_c^i\in\mathcal{Y}_C\)
。另外,每个样本
\(\mathbf{x}\in\mathcal{D}\)
都与一个细粒度类别
\(y_\text{f}\)
相关联,而这些细粒度类别来自于一个未知的细粒度类别集合
\(\mathcal{Y}_F\)
。假设每个细粒度类别
\(y_\text{f}\in\mathcal{Y}_F\)
都与单个粗粒度类别
\(y_c\in\mathcal{Y}_C\)
相关联,即具有唯一的粗粒度父类。细粒度类别的数量
\(K_F = |\mathcal{Y}_F|\)
大于
\(K_C\)
,并且这个值可以在先前得知或进行估计。给定一个粗粒度标记的数据集
\(\mathcal{D}\)
,目标是发现一组细粒度类别
\(\mathcal{Y}_F\)
。因此,希望仅通过粗粒度标记数据集的监督来恢复细粒度标签
\(\tau_F: \mathcal{X} \rightarrow \mathcal{Y}_F\)

Parameterizing the Fine-grained Class Discovery


FALCON
中的一个关键发现是,对细粒度预测和类别关系的组合会产生粗粒度预测。因此,可以利用类别关系将细粒度预测和粗粒度标签联系起来。

使用一个概率分类器
\(f_\theta: \mathcal{X} \rightarrow \Delta^{K_F-1}\)
对细粒度标签
\(\tau_F\)
进行建模,将输入映射到(
\(K_F-1\)
)维的
\(\Delta^{K_F-1}\)
概率单纯形(每个点代表有限个互斥事件之间的概率分布,每个事件通常被称为一个类别) 。然后,对分类器的细粒度预测
\(\mathbf{p}_\text{f}\)

argmax
,可以得到样本的细粒度类别
\(\mathcal{Y}_F\)

\[\begin{equation}
\tau_F(\mathbf{x}) = \text{argmax}_i \, \mathbf{p}_\text{f}^i, \quad \text{where} \quad \mathbf{p}_\text{f} = f_\theta(\mathbf{x}).
\end{equation}
\]

这里,
\(\theta \in \mathbb{R}^d\)
是细粒度分类器的参数,
\(\mathbf{p}_\text{f}\)

\(\Delta^{K_F-1}\)
上的一个点。

利用细粒度预测
\(\mathbf{p}_\text{f}\)
和类别关系
\(\mathbf{M}\)
得到粗粒度预测
\(\mathbf{p}_\text{c}\)

\[\begin{equation}
\mathbf{p}_\text{c} = \mathbf{M}^T \mathbf{p}_\text{f},
\end{equation}
\]

其中,
\(\mathbf{p}_\text{c}\)
是(
\(K_C-1\)
)维概率单纯形
\(\Delta^{K_C-1}\)
上的一个点,
\(\mathbf{M} \in \{0,1\}^{K_F \times K_C}\)
是一个描述细粒度和粗粒度类别关系的二进制矩阵。具体而言,元素
\(\mathbf{M}_{ij}\)
等于
1
表示第
\(i\)
个细粒度类别与第
\(j\)
个粗粒度类别相关联,否则为
0
。由于每个细粒度类别只与一个粗粒度类别相关联,矩阵
\(\mathbf{M}\)
的每一行之和为
1
。因此,
\(\mathbf{M}\)
是一个无向二分图的邻接矩阵,用于建模粗粒度和细粒度类别之间的关系。

FALCON
通过粗粒度监督同时学习细粒度分类器和类别关系,使用交叉熵目标函数(
CE
)来利用粗粒度监督,并学习参数
\(\theta\)
和关系
\(\mathbf{M}\)

\[\begin{equation}
\label{eq:joint_objective}
\mathcal{L}_\text{coarse}(\theta, \mathbf{M}|\mathcal{D}) = \frac{1}{|\mathcal{D}|}\sum_{(\mathbf{x}, y_c) \in \mathcal{D}} \text{CE}(\mathbf{M}^Tf_\theta(\mathbf{x}), y_c).
\end{equation}
\]

通过对离散类别关系
\(\mathbf{M}\)
和连续分类器参数
\(\theta\)
进行联合优化,会导致不稳定且计算成本高昂。为了避免这些问题,论文扩展目标函数并对参数
\(\theta\)
和类别关系
\(\mathbf{M}\)
进行交替优化。

FALCON
中的交替优化过程如图
1
所示,并按以下步骤进行:

  1. 在给定类别关系
    \(\mathbf{M}\)
    的情况下,对由参数
    \(\theta\)
    参数化的细粒度分类器进行训练。
  2. 根据分类器的细粒度预测和粗粒度标签,推断类别关系
    \(\mathbf{M}\)
  3. 该过程重复进行预定义的轮数。

Training Fine-grained Classifier

在固定类别关系
\(\mathbf{M}\)
的情况下,
\(\mathcal{L}_\text{coarse}(\theta, \mathbf{M}|\mathcal{D})\)
变为
\(\mathcal{L}_\text{coarse}(\theta|\mathbf{M}, \mathcal{D})\)
。但仅通过粗粒度标签训练细粒度分类器,无法在一个粗粒度类别中分开细粒度类别。为了克服这个问题,在
FALCON
中引入了额外的目标,鼓励细粒度预测的局部一致性和置信度,从而更好地从粗粒度类别中的分离细粒度类别。

  • Consistent and confident fine-grained predictions

给定输入的最近邻,通过强化最大化输入样本预测与相邻样本预测之间的点积来鼓励细粒度预测一致预测。相应的损失
\(\mathcal{L}_\text{NN}\)
是点积的对数几何平均:

\[\begin{equation}
\label{eq:nn_loss}
\mathcal{L}_\text{NN}(\theta|\mathcal{D}) = \frac{-1}{N L}\sum_{(\mathbf{x},y_c) \in \mathcal{D}} \sum_{\hat{\mathbf{x}} \in \mathcal{N}(\mathbf{x}, y_c)} \ln (f_{\theta_\text{EMA}}(\hat{\mathbf{x}})^T f_\theta(\mathbf{x})),
\end{equation}
\]

其中,
\(\mathcal{N}(\mathbf{x}, y_c)\)
表示给定样本
\(\mathbf{x}\)
在同一粗粒度类别
\(y_c\)
内的最近邻样本集合,
\(\hat{\mathbf{x}}\)

\(\mathcal{N}(\mathbf{x}, y_c)\)
中的一个元素,并且
\(L = |\mathcal{N}(\mathbf{x}, y_c)|\)
。参数
\(\theta_\text{EMA}\)
是在迭代过程中计算参数
\(\theta\)
的指数移动平均值:

\[\begin{equation}
\theta_\text{EMA}^t = \gamma \theta_\text{EMA}^{t-1} + (1-\gamma) \theta^t,
\end{equation}
\]

其中,
\(\gamma\)
是超参数,
\(t\)
代表训练迭代次数。与先前研究不同的是,论文从相同粗粒度类别中检索最近邻样本,并使用
EMA
参数。

损失函数
\(\mathcal{L}_\text{NN}\)
确保了相邻样本之间的细粒度预测的一致性。然而,一致的预测也可能是模棱两可的,这会阻碍形成充分的细粒度类别。因此,通过最小化细粒度预测和目标分布
\(q\)
之间的交叉熵,可以鼓励更有信心地将样本分配到细粒度类别中:

\[\begin{equation}
\label{eq:conf_loss}
\mathcal{L}_\text{conf}(\theta|\mathbf{M}, \mathcal{D}) = \frac{1}{N}\sum_{(\mathbf{x}, y_c) \, \in \, \mathcal{D}} \text{CE}(q_{\theta_\text{EMA}}(\mathbf{x}, y_c), f_\theta(\mathbf{x})).
\end{equation}
\]

细粒度目标分布 $ q $ 利用粗粒度标签 $ y_c $ 的信息来优化各个细粒度类别的分布。使用类别关系 $ \mathbf {M} $ 和参数 $ \theta_\text{EMA} $ 来定义目标分布 $ q $ ,如下所示:

\[\begin{equation}
\label{eq:q_target}
q_{\theta_\text{EMA}}(\mathbf{x},y_c) := \begin{cases}
\frac{\exp(\mathbf{s}^{y_\text{f}} / T)}{Z}, & \text{if } \mathbf{M}_{y_\text{f},y_c} = 1\\
0, & \text{otherwise},
\end{cases}
\end{equation}
\]

其中,
\(T\)
是一个标量温度超参数,
\(\mathbf{s}\)
表示细粒度分类器的逻辑回归。标量
\(Z\)
是一个归一化常数,定义为
\(Z=\sum_{i=1}^{K_F} \mathbf{M}_{i, y_c} \exp( \mathbf{s}^i / T )\)

引入的目标分布
\(q\)
和最近邻的细粒度预测可以被视为一种伪标签形式,如图
1
(左)所示。将损失函数
\(\mathcal{L}_\text{NN}\)
和损失函数
\(\mathcal{L}_\text{conf}\)
合并为一个在细粒度预测的联合损失
\(\mathcal{L}_\text{fine}\)

\[\begin{equation}
\label{eq:cons}
\mathcal{L}_\text{fine}(\theta|\mathbf{M},\mathcal{D}) = \mathcal{L}_\text{NN}(\theta|\mathcal{D}) + \mathcal{L}_\text{conf}(\theta|\mathbf{M},\mathcal{D})
\end{equation}
\]

  • Regularization

为了避免退化解,进一步通过引入最大熵损失函数
\(\mathcal{L}_\text{reg}\)
来稳定训练,该损失函数在聚类相关任务中常被使用。

\[\begin{equation}
\label{eq:reg_ent}
\mathcal{L}_\text{reg}(\theta|\mathcal{D}) = \ln K_F + \sum_{i=1}^{K_F} \overline{\mathbf{p}}_\text{f}^i \ln \overline{\mathbf{p}}_\text{f}^i, \,\, \overline{\mathbf{p}}_\text{f} = \frac{1}{N} \sum_{\mathbf{x} \in \mathcal{D}} f_\theta(\mathbf{x}).
\end{equation}
\]

损失函数
\(\mathcal{L}_\text{reg}\)
有助于避免将所有样本分配到相同的细粒度类别的退化解。

  • Total loss of the fine-grained classifier

将这一切综合起来,
FALCON
优化以下目标来训练细粒度分类器:

\[\begin{equation}
\label{eq:final_cls}
\underset{\theta \, \in \, \mathbb{R}^d}{\text{min}} \left\{ \mathcal{L}(\theta|\mathbf{M}, \mathcal{D}) = \lambda_1 \mathcal{L}_\text{coarse} + \lambda_2 \mathcal{L}_\text{fine} + \lambda_3 \mathcal{L}_\text{reg} \right\},
\end{equation}
\]

其中,
\(\lambda_1, \lambda_2\)

\(\lambda_3\)
是调制超参数。使用细粒度分类器的预测结果之后,
FALCON
学习细粒度和粗粒度类别之间的关系。

Inferring Class Relationships

给定细粒度分类器
\(f_\theta\)
,优化时要求对所有可能的类别关系进行离散优化,以找到最优的
\(\mathbf{M}\)
。主要的困难在于,目标函数既是关于
\(\mathbf{M}\)
的非线性函数,又由于庞大的数据集大小
\(N\)

\(K_C < K_F \ll N\)
)而难以进行评估。然而,离散优化求解器需要对目标函数进行多次评估,且仅适用于特定的问题类别,如线性目标函数。为了克服上述问题,
FALCON
采用了对目标函数近似,从而实现了对类别关系的高效推断。

  • Approximated coarse-grained supervision

首先固定细粒度分类器的参数
\(\theta\)
,并将粗粒度标签的损失
\(\mathcal{L}_\text{coarse}\)
以矩阵形式重新表达:

\[\begin{equation}
\label{eq:cls_matrix_form}
\mathcal{L}_\text{coarse}(\mathbf{M}|\theta, \mathcal{D}) = - \frac{1}{N} \text{tr}(\mathbf{Y}_{oh}^T \ln(\mathbf{P}\mathbf{M})),
\end{equation}
\]

其中,
\(\mathbf{Y}_{oh} \in \{0, 1\}^{N\times K_C}\)
是一个将粗粒度标签表示为
one-hot
向量的矩阵,而
\(\mathbf{P} \in [0, 1]^{N\times K_F}\)
是将细粒度预测聚集到行中的矩阵。对数操作是逐元素进行的,而
\(\text{tr}(\cdot)\)
是迹运算符(矩阵对角线之和)。

为了克服讨论中的挑战,使用泰勒展开对损失
\(\mathcal{L}_\text{coarse}\)
进行近似,并以计算效率高的方式对其进行重新表述:

\[\begin{equation}
\label{eq:linear_coarse_cls}
\mathcal{L}_\text{coarse}^\text{lin}(\mathbf{M}|\theta, D) = - \frac{1}{N} \text{tr}(\mathbf{Y}_{oh}^T\mathbf{P} \mathbf{M}).
\end{equation}
\]

成本矩阵
\(\mathbf{C} = \mathbf{Y}_{oh}^T \mathbf{P} \in \mathbb{R}^{K_C \times K_F}_+\)
有效地编码了粗细类之间的连接强度,每个成本矩阵元素
\(\mathbf{C}_{ij}\)
与粗类
\(j\)
被分配到细类
\(i\)
的样本数量成比例。因此,上述公式的最优解仅保留了粗细类之间最强的连接。需要注意的是,新目标可以比原目标更高效地进行评估,因为矩阵
\(\mathbf{Y}_{oh}^T\mathbf{P}\)
可以预先计算。

  • Regularization

计算目标
\(\mathcal{L}_\text{coarse}^\text{lin}\)
的最优解可能会导致在粗粒度类之间出现严重不平衡的细粒度类分配。因此,引入了一个额外的正则化项,惩罚细粒度类在粗粒度类之间的分配偏差:

\[\begin{equation}
\label{eq:M_bal}
\mathcal{L}_\text{bal}(\mathbf{M}) = \frac{1}{K_C} \text{tr}(\mathbf{M}^T\boldsymbol{1}_{K_F}\boldsymbol{1}_{K_F}^T\mathbf{M}) - \frac{K_F^2}{K_C^2},
\end{equation}
\]

其中,
\(\boldsymbol{1}_{K_F}\)
表示
\(K_F\)
维全为
1
的列向量。因此,
\(\mathbf{M}^T\boldsymbol{1}_{K_F}\)
是一个
\(K_C\)
维的向量,其值对应于每个粗粒度类关联的细粒度类的数量。常数
\(K_F^2/K_C^2\)
修正了损失,使其在平衡分配的情况下为零。

  • Total loss for inferring class relationships

FALCON
通过求解以下优化问题来恢复细粒度类和粗粒度类之间的关系
\(\mathbf{M}\)

\[\begin{equation}
\label{eq:objective_M}
\underset{\mathbf{M} \, \in \, \mathcal{M}}{\text{min}} \left\{ \mathcal{L}(\mathbf{M}|\theta, \mathcal{D}) = \mathcal{L}_\text{coarse}^\text{lin}(\mathbf{M}|\theta, D) + \lambda_M \mathcal{L}_\text{bal}(\mathbf{M}) \right\},
\end{equation}
\]

其中,
\(\lambda_M\)
是一个超参数,用于控制
\(\mathcal{L}_\text{bal}\)
的影响力。集合
\(\mathcal{M}\)
包含所有可能的类别关系:

\[\begin{align}
\mathcal{M} = \{& \mathbf{M} \in \{0, 1\}^{K_F \times K_C} \, |\, \nonumber \\ & \mathbf{M}\boldsymbol{1}_{K_C} = \boldsymbol{1}_{K_F}, \mathbf{M}^T\boldsymbol{1}_{K_F} \geq \boldsymbol{1}_{K_C} \}.
\end{align}
\]

优化公式本质上是一个带有线性约束的整数二次规划问题,该问题涉及仅有
\(K_F\cdot K_C\)
个二进制变量的优化。因此,即使由此产生的问题本质上是
NP-hard
的,也可以迅速地利用现代硬件计算出解。实验证明,
FALCON
可以应用于包含数百个细粒度类别的真实数据集中。

Training on Multiple Datasets

细粒度类别可以以不同的方式被分组成粗粒度类别。例如,可以根据饮食习性(食肉动物与杂食动物)、体型大小(小型与大型)或生物分类学(
Canis lupus

Canis familiaris
)对动物进行分组。因此,尽管对相同细粒度类别的实例进行了聚合,但数据集往往具有不同的标签。
FALCON
可以无缝地应用于在具有不同粗粒度标签的多个数据集上的训练。

具体来说,设
\(\mathcal{D}_l = \{(\mathbf{x}^i, y_c^i)\}_{i=1}^{N_l}\)
是一个数据集,其中
\(\mathbf{x}^i \in \mathcal{X}\)

\(y_c^i \in \mathcal{Y}_C^l\)
,而
\(\mathcal{Y}_C^l\)
是数据集特定的粗粒度类别集合。假设每个数据集
\(\mathcal{D}_l\)
的样本都可以与共享的细粒度类别集合
\(\mathcal{Y}_F\)
中的细粒度类别关联,将来自
\(D\)
个数据集的样本合并为一个组合数据集
\(\mathcal{D}_\text{all}\)

\[\begin{equation}
\mathcal{D}_\text{all} = \cup_{l=1}^D \{ (\mathbf{x}, y, l) \, | \, (\mathbf{x}, y) \in \mathcal{D}_l \}.
\end{equation}
\]

\(\mathcal{D}_\text{all}\)
中的每个数据点都是一个由输入、粗粒度标签和样本来源数据集的索引组成的三元组,通过建模
\(D\)
个数据集特定的映射
\(\mathbf{M}_l\)
来扩展:

\[\begin{equation}
\mathcal{L}_\text{coarse}(\theta, \mathbf{M}_1, \dots, \mathbf{M}_D | \mathcal{D}_\text{all}) =\\ \frac{1}{|\mathcal{D}_\text{all}|}\sum_{(\mathbf{x}, y_c, l) \in \mathcal{D}_\text{all}} \text{CE}(\mathbf{M}_l^Tf_\theta(\mathbf{x}), y_c) .
\end{equation}
\]

因此,将多个数据集集成到
FALCON
框架中只需要推断出
\(D\)
个数据集特定的类别关系
\(M_l\)
。与单个数据集的情况类似,
FALCON
通过解决公式
14
推断出数据集特定的类别关系。所有
\(D\)
个离散优化问题是相互独立的,可以并行求解。

Experimental Setup


Datasets & Metrics

  • Datasets

在八个图像分类数据集上评估了
FALCON
,包括
Living17

Nonliving26

Entity30

Entity13

tieredImageNet

CIFAR100

CIFAR-SI

CIFAR68
数据集。数据集
Living17

Nonliving26

Entity30

Entity13
来自于
BREEDS
基准测试。

  1. 对于
    tieredImageNet
    数据集,将训练、验证和测试的分类体系合并为一个包含
    \(608\)
    个细粒度类别和
    \(34\)
    个粗粒度类别的单一数据集。
  2. 对于
    CIFAR100
    数据集,使用具有
    \(20\)
    个粗粒度类别和
    \(100\)
    个细粒度类别的原始标签。原始的
    CIFAR100
    数据集中,每个粗粒度类别都有相同数量的细粒度类别,每个细粒度类别中也有相同数量的样本。因此,额外引入了两个不平衡版本的
    CIFAR100
    数据集,命名为
    CIFAR68

    CIFAR-SI
    数据集。

  3. CIFAR68
    数据集的情况下,从原始数据集中删除了
    \(32\)
    个细粒度类别,以使粗粒度类别中的细粒度类别数量不平衡。

  4. CIFAR-SI
    数据集的情况下,从每个细粒度类别中删除了高达
    \(70\%\)
    的样本,实际上导致了样本分布的不平衡。

此外,为了表明
FALCON
具有广泛的适用性,考虑了来自生物领域的单细胞
RNA
测序数据集。在从
COVID-19
患者血液样本中收集的
PBMC
数据集上评估了
FALCON
。任务是在给定粗粒度细胞类型的情况下将细胞分类为细粒度细胞亚型。根据对应于细粒度标签的真实细胞亚型对该方法进行评估。
PBMC
数据集极度不平衡(基尼系数大于
0.5
)。在单细胞数据上进行了转导性设置下的性能评估。

所有考虑的数据集的概述如表
1
所示。缩写
L17
代表
Living17

N26
代表
Nonliving26

E30
代表
Entity30

E13
代表
Entity13

C100
代表
CIFAR100

C68
代表
CIFAR68

CSI
代表
CIFAR
样本不平衡,
tIN
代表
tieredImageNet

PB
代表
PBMC

  • Metrics

在没有细粒度基准标签的情况下训练了
FALCON
和基线模型。因此,报告细粒度聚类准确度作为评估指标。

\[\begin{equation}
\text{Acc} = \underset{p \in \mathcal{P}(\mathcal{Y}_\text{f})}{\max} \frac{1}{|D|} \sum_{i=1}^{|D|} \, 【 y_\text{f}^i = p(\hat{y}^i_\text{f})】.
\end{equation}
\]

在这里,
\(\mathcal{P}(\mathcal{Y}_\text{f})\)
是所有细粒度类别标签的排列组合。在实践中,可以使用匈牙利算法高效地计算该度量。此外,我们还报告了调整兰德指数(
ARI
)。由于
FALCON
还学习了类别关系,我们使用图编辑距离(
GED
)报告了学习到的标签关系与地面真相图之间的差异。图编辑距离计算必须添加或移除的节点和边的数量,以使其匹配目标图。

Baselines

由于没有专门为粗粒度监督下细粒度类别发现的方法,将
FALCON
与可以应用于该设置的方法进行比较,包括改编为细粒度类别发现的聚类和跨粒度少样本方法。

SCAN
是一种深度聚类方法,直接将其应用于细粒度类别发现,通过对数据进行聚类。然而,
SCAN
在训练过程中无法利用有关粗粒度类别的信息。因此,额外通过在同一粗粒度类别中强制保持邻居之间的一致预测来对
SCAN
进行了改进。这种改进使得
SCAN
能够利用粗粒度监督。我们将这个基准方法称为
SCAN-C

论文进一步将跨粒度少样本学习方法作为基线进行比较。
ANCOR
是一种跨粒度少样本学习方法,它学习细粒度表示空间。因此,对提取的特征运行K均值聚类以恢复细粒度预测,使用相同的方法来改编
SNCA

SCGM
是一种少样本学习方法,可直接应用于细粒度类别发现,因为它提供了细粒度预测。

论文还加入
GEORGE
,它通过分布鲁棒优化粗分类目标。
GEORGE
只学习细粒度表示空间,因此我们再次运行K均值算法来恢复细粒度预测。

最后,可以通过对细粒度标签进行经验风险最小化(
ERM
)来确定性能的上限。

Implementation Details

对于来自
CIFAR
数据集的小尺寸图像,使用
ResNet18
作为骨干网络,并对剩余的五个图像数据集使用
ResNet50
作为骨干网络。使用自监督预训练方法
MoCoV3
对所有方法(即
FALCON
和所有基线方法)进行初始化。在训练过程中,更新模型的所有参数。将输入的弱增强与
\(\theta_\text{EMA}\)
配对,将强增强与
\(\theta\)
配对。使用自监督特征表示之间的距离来检索最近邻。在
Optuna
中使用
TPE
算法对
CIFAR100
数据集进行超参数搜索。使用
Gurobi
解决离散优化问题。

在单细胞数据的情况下,使用具有
4
个线性层和
ReLU
激活的随机初始化
MLP
。通过计算前
2k
个高度变异基因的距离来检索最近邻。

Experimental Evaluation




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

LINQ介绍

语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。数据查询历来都表示为简单的字符串,没有编译时类型检查或 IntelliSense 支持。此外,需要针对每种类型的数据源了解不同的查询语言:SQL 数据库、XML 文档、各种 Web 服务等。借助 LINQ,查询成为了最高级的语言构造,就像类、方法和事件一样。

安装.NET 9

需要体验LINQ的新增功能前提是需要安装.NET9环境,.NET 9是.NET 8的继任者,特别侧重于云原生应用和性能。作为标准期限支持 (STS) 版本,它将在 18 个月内受到支持。

验证是否安装成功

dotnet --list-sdks

.NET 9 SDK目前在Visual Studio 2022中不适用

注意:.NET 9 SDK目前在Visual Studio 2022中不适用,所以无法在Visual Studio2022中选择.NET 9 为目标框架。

VS Code中编写ASP.NET Core Web应用

VS Code中创建.NET 9控制台应用

从VS Code中打开终端

运行以下命令:

dotnet new console --framework net9.0 --use-program-main

运行应用

dotnet run

CountBy方法

CountBy这个方法允许开发者按键来聚合集合中的元素,并计算每个键的出现次数。这使得计算某个数据集中特定元素的频率变得非常简单。

    public static void CountByExample()
    {
            //这里wordCounts是一个字典,包含每个单词及其出现次数的键值对
            var sourceText = "This is a test text. This is only a test. This is the best. This,This,This";
            KeyValuePair<string, int> mostFrequentWord = sourceText
            .Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
            .Select(word => word.ToLowerInvariant())
            .CountBy(word => word)
            .MaxBy(pair => pair.Value);

            Console.WriteLine($"最常见的词是:'{mostFrequentWord.Key}'  {mostFrequentWord.Value}");
   }

输出结果:

AggregateBy方法

AggregateBy这个方法提供了更强大的聚合功能。开发者可以定义一个聚合逻辑(如求和、平均值等),并按键进行聚合。该方法在需要基于键对集合中的元素进行复杂计算时非常有用。

    public static void AggregateByExample()
    {
        (string id, int score)[] data =
            [
                ("0", 88),
                ("1", 5),
                ("2", 4),
                ("1", 10),
                ("6", 5),
                ("4", 10),
                ("6", 25)
            ];

        // aggregatedData 是一个序列,包含按姓名分组并计算总分的元素
        var aggregatedData =
            data.AggregateBy(
                keySelector: entry => entry.id,
                seed: 0,
                (totalScore, curr) => totalScore + curr.score
                );

        foreach (var item in aggregatedData)
        {
            Console.WriteLine(item);
        }
    }

输出结果:

Index<TSource>(IEnumerable<TSource>)
方法

借助
Index<TSource>(IEnumerable<TSource>)
,可以快速提取可枚举项的隐式索引。现在,可以编写代码(如以下代码片段)来自动为集合中的项编制索引。

    public static void IndexExample()
    {
        var lines = new List<string> { "First line", "Second line", "Third line" };
        foreach (var (index, line) in lines.Index())
        {
            Console.WriteLine($"Line {index + 1}: {line}");
        }
    }

输出结果:

参考文章

C#/.NET/.NET Core拾遗补漏

单个48TB大小SQL Server数据库备份导致日志文件无法截断


SQL Server 版本:SQL Server 2019
背景
在一个48T大小的单数据库环境中,采用简单恢复模式,日志文件大小限制为600G。执行一次完整备份时,耗时超过12小时,导致日志文件无法截断并达到上限,后续事务无法正常写入,导致整个数据库不可用。



问题现象
LDF日志文件中的虚拟日志文件 (VLF) 全部为活动状态,导致日志无法正常截断。由于日志文件大小达到了600G的限制,后续的事务写入失败,导致数据库操作停滞。



排查思路
排查思路一般有下面几个:
1、大事务导致日志无法截断:可能是未提交的大事务阻止了日志的截断。
2、特殊环境:如复制 (Replication)、镜像 (Mirroring)、可用性组 (AG)、变更数据捕获 (CDC) 等场景下,备用端异常会导致主端无法截断日志。
3、未及时备份事务日志:在完整恢复模式下,未及时备份事务日志导致日志文件增长。
4、数据库恢复时间设置:修改过数据库恢复时间,可能导致checkpoint延迟,从而延长日志文件的截断时间。



排查步骤
按照下面思路进行逐一排查
1、确认数据库恢复模式:数据库为简单恢复模式,排除事务日志备份问题(思路3)。
2、检查运行环境:数据库为单机模式,排除复制、镜像、AG等场景(思路2)。
3、检查长时间事务:使用 `DBCC OPENTRAN` 检查,未发现长时间运行的事务,排除大事务问题(思路1)。
4、检查数据库恢复设置:`TARGET_RECOVERY_TIME` 和 `recovery interval` 为默认值,排除恢复时间问题(思路4)。
5、检查阻塞情况:未发现阻塞问题。
6、检查SQL Agent作业:除了完整备份外,未发现其他作业运行。
7、检查写入逻辑:与开发人员沟通得知,该数据库为日常批量数据写入,日志写入量超过400G。





问题原因分析
这里面我们需要从数据库完整备份的原理入手,解剖备份的细节,分析原因,一般来说,数据库完整备份分为两个阶段:
1、第一阶段:记录备份开始时的LSN,生成快照式备份。
2、第二阶段:快照备份结束后,记录最新的LSN,并将这两次LSN之间的事务日志写入备份。
由于数据库较大,备份时间超过12个小时,导致备份操作一直处于第一阶段,无法进入到第二阶段。这期间,日志文件中的事务无法截断,12小时内的事务量超过600GB,导致LDF日志文件被打爆。



优化建议
最后的解决方案是,开启SQL Server 2019的ADR(加速数据库恢复)功能,完整备份时候,日志文件只有少量增长,问题解决。
启用 ADR 的步骤
确保使用的是 SQL Server 2019 或 SQL Server 2022,因为 ADR 是从 SQL Server 2019 开始引入的。

--开启数据库的ADR功能
ALTER DATABASE [YourDatabaseName]
SET ACCELERATED_DATABASE_RECOVERY = ON;GO


--检查 ADR 状态
SELECTname, is_accelerated_database_recovery_onFROMsys.databasesWHERE name = 'YourDatabaseName';--关闭 ADR
ALTER DATABASE [YourDatabaseName]
SET ACCELERATED_DATABASE_RECOVERY = OFF;GO


这里有如下几个优化思路:
1、多文件备份:将默认的单文件备份改为多文件备份,可以提升备份效率,缩短备份时间。(之前介绍过SQL Server的一个不显眼的功能备份文件分割)
2、升级SQL Server版本:升级到SQL Server 2019或SQL Server 2022,启用ADR(加速数据库恢复)功能,通过SLOG实现日志及时截断。
3、业务拆分:尽管不现实,但从业务上进行拆分也可以减少单一数据库的事务量。


总结

在SQL Server 2019中,一个48TB数据库因备份耗时过长,导致日志文件无法截断并达到上限,阻碍事务写入。
本文介绍了多种解决方案包括多文件分割备份、启用数据库ADR功能。
最终启用数据库ADR功能解决了问题。这里要注意的是,一定要尽量使用最新的数据库版本,例如SQL Server 2019或者SQL Server 2022,
保证能使用到最新功能,可以摆脱很多麻烦。




补充:数据库完整备份原理
在完整备份过程中,即使数据库处于简单恢复模式,备份依然会拷贝未提交事务的日志。对于长时间运行的事务,备份会包含足够的日志信息来撤销这些未提交的事务。
因此,即使数据库的MDF文件较小,日志文件(LDF)也可能会导致备份文件非常大。
这与MySQL的Xtrabackup的原理几乎是一样的,备份开始时生成一个开始LSN,结束时生成一个结束LSN,如果有未提交事务,MySQL8.0的
undoxx
文件会非常大




参考文章
https://learn.microsoft.com/en-us/sql/relational-databases/accelerated-database-recovery-concepts?view=sql-server-ver16
https://www.mssqltips.com/sqlservertip/5971/accelerated-database-recovery-in-sql-server-2019/
https://medium.com/ricos-note/accelerated-database-recovery-a7f0d30b1e0


本文版权归作者所有,未经作者同意不得转载。