0%

Multi-gate Mixture-of-Experts (MMoE)

本笔记为王树森发表在youtube频道推荐系统的视频课程学习随笔,笔记中图片亦截取自王树森视频课件,原始资料出处为王树森youtube视频

网络结构

首先上一张图,说明网络的完整结构。

image.png

输入特征

MMoE的输入特征由 用户特征物品特征统计特征场景特征构成。 其中,用户特征和物品特征与召回层的特征一致。

专家网络

专家网络可以有多个,上图中只画出了三个,但是在实践中可能尝试四个五个或者多个。每个专家网络都不共享参数,他们是独立的。各个专家网络输出维度相同的向量。

专家网络的数量是一个超参数,通常会试一试四个或者八个。

指标网络

指标网络有多个,其网络个数与使用的指标的数量一致,例如上图我们假设P输出为点击率,Q输出为点赞率,当然,再添加网络还可以输出转发率等等。每一个指标网络是独立的,不共享参数,且他们的输出层都使用softmax作为激活函数,所以他们的输出的向量含义可以解释为概率或者权重,其维度与专家网络的个数一致。

上层网络(预测网络)

上层网络的输入是下层网络(专家网络,指标网络)的输出组合。先上一个网络结构图。

image.png

我不知道上层的每个分支网络应该叫做什么名字,我就在笔记中擅自叫它们 预测网络 。 预测网络的输入为指标网络的输出作为权重,将每个专家网络的输出加权平均, 所以预测网络的输入维度与专家网络的输出维度一致。

预测网络全连接层可以有一层,也可以有多层,其输出为一个标量数值,代表参考了所有专家网络后对该指标的预测值。

极化现象

什么是极化现象

MMoE网络训练中容易出现极化现象。 所谓极化现象,就是指标网络的softmax输出中,其中一个元素非常接近于1而其他元素接近于0。此种输出下,与专家网络进行加权平均时,会使的部分专家网络的输出被忽略(此指标权重接近0),只选取个别专家网络的输出,而且,不同的指标网络会选择不同的专家网络输出。这种状况下,多专家网络失去了其意义。我们希望指标网络均衡的考虑各专家网络的意见。

image.png

如何解决极化现象

  1. 在训练时,对softmax的输出使用dropout。此方法基本可以避免极化。

总结

  1. MMoE是一个排序层的网络,它对召回的结果进行输出分数以供排序。
  2. MMoE网络的思想是建立多个专家网络,让这些网络根据输入提出自己的“意见”,判别网络综合所有专家的意见对它所负责的指标进行预测,根据真实的指标值进行监督学习,从而训练出一个可以近乎准确预测指标的网络。
  3. MMoE会发生极化现象,要避免极化现象的发生,可以在Softmax层使用dropout方案。
  4. MMoE网络不一定对指标有提升,实际的工程上,也有很多MMoE不生效的情况。

双塔模型

本笔记为王树森发表在youtube频道推荐系统的视频课程学习随笔,笔记中图片亦截取自王树森视频课件,原始资料出处为王树森youtube视频。其中有我从其他途径得到的一些内容和我自己的见解,未必全部都准确正确。

双塔模型因为两个垂直网络象形而得名, 这两根塔分别是用户网络和物品网络。

结构

双塔模型因为两个垂直网络象形而得名, 这两根塔分别是用户网络和物品网络。实际的训练过程中,可能存在不止一根“塔”,但是只有两类。

用户网络

用户网络将用户ID做Embedding, 离散特征做Embedding, 剩余的一些连续特征先做归一化和分桶等处理方式然后concatenate到一起输送给神经网络。得到用户的隐向量表征。向少分量的离散向量例如性别之类的,就不用做embeding,直接使用onehot即可。

用户网络

物品网络

物品网络和用户网络基本一样,同样的需要把物品ID做Embedding。

我个人感觉把ID做Embedding好没有道理,本质上物品的ID,它并不直接包含任何特征,可能仅仅是为了缩小输入规模所以做embeding?

物品网络
双塔模型

双塔模型的训练

  • Pointwise: 独立看待每个正样本、负样本,做简单的二元分类
  • Pairwise:训练时使用一个用户特征向量,一正一负共两个物品特征向量。
  • Listwise:训练时使用一个用户特征向量,一正多负多个物品特征向量。

正负样本的选择

  • 正样本: 正样本的选择其实很明确,用户点击(点赞收藏转发)过的肯定是正样本。
  • 负样本:负样本按级别分几类,简单负样本,困难负样本等。

下面先从总体概览的角度上看一下样本到底有哪些,是怎么来的。

以全部的库存物品作为样本的全集,那么:

  • 没有被召回的物品,大概率就是不感兴趣的了,为负样本。

  • 被召回的物品,我们需要再分情况讨论:

    • 被曝光的且被用户点击/点赞/收藏 的,自然为正样本,这个无疑是无可置疑的。
    • 被曝光的,但是用户未点击的。
    • 未被曝光的(通过召回但却被粗排精排过滤掉的)。

Pointwise 训练

把找回看作二元分类任务。

  • 对于正样本,鼓励 \(cos(a, b)\) 接近+1。
  • 对于负样本,鼓励 \(cos(a, b)\) 接近-1。
  • 控制正负样本的数量为 1:2或者1:3 (经验数值)。

Pairwise 训练

输入是一个三元组,输入两个样本数据(一正一副),一个用户数据。 用户数据的网络输出分别与两个物品样本数据的网络输出做cos距离。这种三元训练与人脸识别三元损失中是一样的。

这里需要提一下,图中绘制了两个物品网络,实际上它们是参数共享的,应为一个结构,样本在其中传输了两次。

Pairwise训练

基本想法:鼓励\(cos(a, b^+)\) 大于\(cos(a, b^-)\)

使用cos距离时,输出值约接近1则表明两个向量越接近,而-1则表明越远。

  • 如果\(cos(a, b^+)\)大于\(cos(a, b^-) + m\),则没有损失。
  • 否则, 损失等于 \(cos(a, b^-) + m - cos(a, b^+)\)

Triplet hinge loss损失:

\[ L(a, b^+, b^-) = \max(0, cos(a, b^-) + m - cos(a, b^+))) \]

其中,向量a是用户向量,向量\(b^+\)\(b^-\)分别是物品的正负样本向量。

训练过程即使用梯度下降算法对损失函数最小化。

Triplet logistic loss损失:

\[ L(a, b^+, b^-) = \log(1+\exp[\sigma\cdot(\cos(a, b^-)-\cos(a, b^+))]) \]

Listwise训练

Listwise方法训练时,每次取一个正样本和多个负样本。

  • 一条数据包含:

    • 一个用户,特征向量记作\(a\)
    • 一个正样本,特征向量记作 \(b^+\)
    • 多个负样本,特征向量记作\(b^-_1,\cdots,b^-_n\)
  • 鼓励\(\cos(a, b^+)\) 尽量大。

Listwise

图示已经很清晰的揭示了Listwise训练的原理, 让正样本对应输出的概率接近于1,而所有负样本输出的概率接近于0,使用交叉熵损失即可进行训练。

总结

  • 用户塔和物品塔各输出一个向量。

  • 两个向量的余弦相似度作为兴趣的预估值。

  • 三种训练方式:

    • Pointwise: 每次一个用户、一个物品(可正可负)。
    • Pairwise: 每次一个用户、一个正样本、一个负样本。
    • Listwise:每次一个用户、一个正样本、多个负样本。

一个错误的网络结构示例

先上一个反例图,然后再分析其中原因。

wrong_tower

上图中,明显的,在我们获取了用户和物品的特征向量之后,又将它们拼接通入了一个神经网络获得输出。但从是否work的角度来说是没有问题的,但是结合工程实际就不行了,表现在:如果我们获得了一个用户的特征向量,想要获得最近邻的物品,我们需要把每个物品的特征向量都拿来通过神经网络计算兴趣值,往往工程中物品数量是个巨大的数字,这种做法无法满足实时性的要求。

所以,我们在召回模型中,必须能够通过用户特征可以快速的获得最近邻的物品,那么我们必须满足不能在查表之后再经过神经网络了。

Linux重启机器后亮度无法恢复的问题

背景

使用Ubuntu已经好些年头了,一直都有启动机器后无法恢复亮度的问题。之前大多是使用台式电脑,基本无需解决,调整到最大亮度后,直接设置显示器,调整到合适的亮度就不再管了。现在换用了笔记本,这种策略不再好用。两方面, 一是笔记本内置显示器不带单独的配置,无法在外部调整亮度。二是笔记本的使用场景不固定,客厅,卧室,阳台等,场景不同受到的环境光照影响也就不同,一直不调节亮度这种就很难受了,遂决定解决。

问题

问题描述

环境: Ubuntu20.04 kenal 5.11+, Gnome3 硬件: cpu amd4700U 核心显卡,无板载和独立显示芯片。其他硬件对此问题无影响。

亮度调节:菜单调节显示器亮度有效,可以正确调整亮度。但是每次重新开机后,亮度都会被设置成最大值。菜单划条位置显示在1%左右的位置。调节后,实际亮度与菜单划条一致。(说的可能不是很明确,就是每次稍微一调节,就会导致亮度忽然变很暗,因为亮度划条的位置几乎初始在最暗的位置上)

日志内容

使用journalctl查看日志内容:

1
journalctl -b0 -p4

得到输出如下, 无关内容已经被删除。

1
2
3
4
5
6
7
8
9
2月 04 18:22:47 redmi-book kernel: acpi PNP0C14:01: duplicate WMI GUID 05901221-D566-11D1-B2F0-00A0C9062910 (first instance was on PNP0C14:00)
2月 04 18:22:48 redmi-book kernel: amdgpu 0000:03:00.0: Direct firmware load for amdgpu/renoir_ta.bin failed with error -2
2月 04 18:22:48 redmi-book kernel: ath10k_pci 0000:01:00.0: failed to fetch board data for bus=pci,vendor=168c,device=003e,subsystem-vendor=11ad,subsystem-device=0847 from ath10k/QCA6174/hw3.0/board-2.bin
2月 04 18:22:49 redmi-book systemd-backlight[627]: Failed to get backlight or LED device 'backlight:acpi_video0': No such device
2月 04 18:22:49 redmi-book systemd[1]: systemd-backlight@backlight:acpi_video0.service: Failed with result 'exit-code'.
2月 04 18:22:49 redmi-book systemd[1]: Failed to start Load/Save Screen Backlight Brightness of backlight:acpi_video0.
2月 04 18:22:49 redmi-book systemd-backlight[630]: amdgpu_bl0: Failed to write system 'brightness' attribute: No such device or address
2月 04 18:22:49 redmi-book systemd[1]: systemd-backlight@backlight:amdgpu_bl0.service: Failed with result 'exit-code'.
2月 04 18:22:49 redmi-book systemd[1]: Failed to start Load/Save Screen Backlight Brightness of backlight:amdgpu_bl0.

从日志来看,背光亮度相关的日志内容有两条:

  1. acpi_video0 无法找到。
  2. amdgpu_bl0 无法找到。

分析

针对上面日志提到的内容,我查看ls /sys/class/backlight/。发现其中仅有一个文件夹,它是amdgpu_bl0, 根本没有acpi_video0。所以,对于第一个报错日志来说,确实正确反馈了这个情况。那么针对此条,本文方案1进行了修复,可以达到修复的目的。

但是,对于日志第二条,明显对应不上,我这里确实是有这个设备的,它应该正确的工作才是。

解决方案

针对现象的有效解决方案有两个,他们有各自的问题。后面就逐个说明一下。

方案1

原理

针对第一条日志内容,从中看来,服务尝试使用acpi_video0来设定亮度,然而我的机器上没有这个。看起来acpi应该是某个规范或者是总线类型,先搞清楚它是什么。阅读了 acpi介绍以后,发现它是os和硬件中间的一层抽象,那么没有它的话,可能是我机器就没有启用acpi,或者没有给他配置。那么问题就简单了,我们添加和检查一下配置就是了。

操作

添加内核参数 acpi_backlight=video 在/etc/default/grub文件中,GRUB_CMDLINE_LINUX_DEFAULT条目后添加内和参数acpi_backlight=video保存。 重新生成grub配置,并更新。

1
grub-mkconfig -o /boot/grub/grub.cfg

重启。

修复情况

重启后观察,可以正确恢复关机前的亮度了,且日志中报错的信息消除了。

副作用

但是,dmesg 日志内容中多出了一条无法识别显卡型号的日志,我不确定此时是否使用了正确的显卡驱动(或者全特性支持的驱动)。从视频测试上来看,播放youtube2k分辨率的视频,cpu占用率大概在20%-30%之前浮动。之前应该是在15%-20%浮动,看起来,这么做了之后多少还是影响了显示性能的。没有其他明显副作用。

方案2

原理

一开始我是懵逼的,于是我按照日志李的服务手动start了一下,竟然发现它成功了,而且没有任何报错,而且成功设置了亮度。于此时,我几乎可以肯定是这个服务的启动时机有问题了,也许这个服务被执行的时候,amdgpu的显卡驱动或者什么的还没有初始化好,所以导致了失败,那么,把时机往后延一下,应该就能解决问题了。

操作

  1. /etc/systemd/system/目录下,创建一个新的systemctl unit, 我把它命名为 backlight-restore.service, 当然,这里名字不重要。

  2. 编辑unit内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [Unit]
    Description=Dummy service for attempting to start the problematic amdgpu_bl0 service

    [Service]
    Type=simple
    ExecStart=systemctl start systemd-backlight@backlight:amdgpu_bl0

    [Install]
    WantedBy=multi-user.target

    实际上这个服务就是在机器启动后再次调用恢复亮度的服务而已,只是它的时机比较晚。

  3. 启动这个service sudo systemctl start backlight-restore.service

  4. 检查这个service的启动输出和状态 sudo systemctl status backlight-restore.service

  5. 没问题了的话,就设置为自启动 sudo systemctl enable backlight-restore.service

  6. 重启机器 sudo reboot

修复情况

重启后发现,亮度可以正常恢复了。但是之前的日志报错还是在(预期中)。

副作用

暂未发现有什么副作用。

其他方案

当然还有其他几个方案。

其中包括

1.编辑内核模块文件,内核启动时加载amdgpu模块。但此方案在内核升级时就需要再操作一次,ubuntu也经常更新内核,所以我不采取。 2.网上流行的一种非主流做法,编辑rc.local文件,跳过系统机制,在启动后手动 往亮度文件中写值。但是rc.local已经被标记为废弃了,不再是标准内容说不准哪个版本就给作废了呢,也不采取。

遗留问题

以上两种方案多多少少都有些问题,方案1会降低一些gpu性能,自然也就降低了一些续航,我个人对电池续航非常看重,所以不推荐方案1. 方案2采用事后补偿的方式,虽然解决了问题,但是启动前阶段并没有接入,所以报错的日志依然会出现。

总结

我个人使用了方案2,暂时还不错。

Java中的strictfp关键字

背景

绝大多数Java程序员都没有使用过strictfp关键字, 大多数的Java程序员甚至都不知道这个关键字。 其原因跟strictfp的作用有关, 因为绝大多数的Java程序员都在做x86/64平台下的web应用,抑或也使用其他服务平台。 仅从其后,同样不太流行的Java关键字还有transient

strictfp的作用

正是因为strictfp的作用导致Java程序员们甚至都没用过它。

在更早的Java版本中,浮点数本身就是严格的,但近代java版本(也不怎么近,就java1.2和之后)为了更高效的运算,默认情况下,Java的浮点数运算是平台相关的,。jvm会依据当前平台的机器特性选用适合的浮点数处理方式,而不是严格按照标准进行计算,例如,在一些低性能嵌入式设备上,Java会利用更高效的浮点数运算指令进行浮点数计算,它的结果和精度都与高性能的硬件上不同。

由于Java实现不再广泛使用x87浮点,因此有一个主动提议再次使所有浮点运算严格,有效地恢复1.2之前的语义。

strictfp允许程序员强制指定浮点数的运算遵循严格规范。当运行在不直接支持指令运算的硬件上时,Java会使用软件的形式去进行严格浮点数计算,这样会大大降低浮点数运行效率,但好处也是显而易见的, 浮点数运算的结果可知可控且各平台一致

在没有溢出或下溢的情况下,有或没有strictfp的结果没有差异。如果重复性是必要的,则strictfp修饰符可用于确保在所有平台上的相同位置发生溢出和下溢。如果没有strictfp修饰符,中间结果可能会使用更大的指数范围。

该strictfp修改由代表所有的中间值作为IEEE单精度和双精度值,如发生在早期版本的JVM实现这一目的。

strictfp 关键字的用法

strictfp可用于修饰 类、非抽象方法或者接口。 除此之外均不可用,尤其是直觉上的用于属性和构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public strictfp class ScientificCalculator {
// ...

public double sum(double value1, double value2) {
return value1 + value2;
}

public double diff(double value1, double value2) {
return value1 - value2;
}
}

public strictfp void calculateMarksPercentage() {
// ...
}

public strictfp interface Circle {
double computeArea(double radius);
}

strictfp的修饰作用不会延伸到其子类(实现类)和子类方法中,除非是内部类。被strictfp修饰的类的子类如果要保持严格浮点计算的特性,那么,需要在它本身再次声明strictfp。 联系到抽象方法无法使用strictfp修饰,这一点比较好理解。

但是对于接口,strictfp存在的意义仅为声明其默认方法为浮点运算严格的,一旦接口没有默认方法,它被strictfp修饰意义就仅剩其内部类了,然而接口的内部类使用并不常见。

何时使用strictfp

  1. 当在意浮点数运算的确定性和精度时,需要使用strictfp

在有些平台上,浮点数为了快速计算并不严格遵循IEEE754的规范,这可能导致浮点数运算结果在精度上与规范要求的不同,当我们的代码需要对这部分作出保证时,就要使用strictfp了。

  1. 代码需要运行在不同的平台上产生相同的行为时,需要使用strictfp

随着分布式技术的投入使用和异构平台协同,同样的代码可能同时运行在不同平台的机器上协同工作,此时保证其计算结果的相同就尤为重要了,这种情况下,需要使用strictfp

strictfp会影响代码性能么?

会,但不是全部

在回答这个问题上,我没有做大量的实验来验证,但从查阅到的资料和对strictfp实现的分析中得出这个结论。

jvm在目标平台上往往会采取更高效的浮点数运算方法,这一般意味着硬件提供的快速的浮点数运算指令。这类指令往往不遵循IEEE规范但更高效。高效正式这类指令存在的重要原因(否则为什么不严格遵守规范呢?),高效不一定体现在计算速度上,也可以体现在硬件设计复杂性更低上,可以使用更少的门电路实现乘法器。

使用了strictfp修饰类后,往往会造成jvm无法使用这类更加“高效”的指令,在简陋指令平台甚至需要使用软件控制来完成浮点运算,这样往往会大幅降低计算效率(一旦使用软件控制,效率降低可达几十倍)。

但是,对于高端的现代平台,往往都提供了高效且严格遵循IEEE754规范的乘法器指令, jvm即使没有被告知strictfp本身就会使用这些指令,所以strictfp 在这类平台上是不会影响执行效率的。

背景

现在要实验强化学习算法的效果, 需要很老的一个基于Unity的项目作为环境, 从版本库种好容易找到了这个项目, 使用新版本的Unity无法正常打开只能安装较旧版本的Unity Editor, 但是我的系统盘空间不足了, 使用大文件查找找到了一堆看似无用的大文件夹,然后无情删除。成功安装了旧版本的编辑器。

然后问题就来了, IDE里面代码整片整片的爆红, 编译不过, 看提示是需要 dotNetFrameWork 3.5。 正常来说,这个运行时是在Windows10安装时自带的, 可能被我一顿操作删除没了。

遇到问题的过程

既然说缺少了dotNetFrameWork特定版本, 那就去下载安装, 微软提供了一个官方的地址,其中列举了各个版本的dotNetFrameWork

1
https://dotnet.microsoft.com/download/visual-studio-sdks?utm_source=getdotnetsdk&utm_medium=referral

按照提示下载了对应的在线安装器。

安装过程需要下载 1.0和2.0 版本, 反正也只能点同意, 但是还是出问题了, 无法安装,错误码是 0x80240234 很奇怪的一个错误码,官方并没有说明这个错误码是什么含义。

解决的过程

既然需要安装 .Net 那么目标很明确,也就不解决这是什么错误了,就是想办法安装上就是了。

独立的安装器既然都无法安装,那么也对Windows自身的安装不抱幻想,不过总归还是要试一试。

0. 尝试 重新安装Visual Studio

在Visual Studio的功能模块列表中有一项是 .NetFrameWork3.5 SDK 的项目, 我认为如果缺失了.NetFrameWork3.5, 那么在安装VisualStudio的过程中,这个可以被安装上, 可是我的机器已经有一个VisualStudio的版本了, 于是面临卸载重装。

卸载很快就完成了,但是新安装是个非常耗时的过程,在等待下载的过程中,先尝试其他方法解决。幸而其他方法生效了, 此方案没有用上,最终也不知道是不是有效。

1. 尝试 启用或关闭Windows功能

通过控制面板 -> 程序和功能 -> 启用或关闭Windows功能 打开Windows功能编辑页面。 果然windows_function_editor

.Net3.5显示未勾选状态了。直接勾选上,点击确定... 果然还是一样的报错,无法完成。

2. 尝试 清空SoftwareDistribution 目录后更新

我发现SoftwareDistribution目录一共存在两个位置, 一个是C:\ProgramData\SoftwareDistribution 另一个是C:\Windows\SoftwareDistribution 都把它们清空,然后重新开启功能, 失败。 使用独立安装器, 依然失败。

3. 使用 Windows 安装媒体

这个方法成功了。

  1. 准备安装文件。 就是安装Windows时使用的光盘镜像文件(.iso)。
  2. 解压安装文件的 source/sxs 目录到指定位置。 windows10的文件管理器可以直接挂载iso作为虚拟驱动器,无需额外的压缩工具,就是一个简单的复制过程。
  3. 使用管理员权限打开命令提示符窗口,输入 Dism /online /enable-feature /featurename:NetFx3 /All /Source:${sxs目录绝对路径} /LimitAccess 等待结束,这次成功了。

总结

虽然本文篇幅较小,但是其中耗费的时间和耽误的功夫着实不小。Windows缺失组件的问题本来就是相当棘手的问题, 再在Windows官网上寻找方案, 下载必要的工具,依次去尝试,中间由于工具提示数次重启机器,耗时半天。 幸好,最终总算是解决了, 在此做个记录,以后再遇到这个问题直接使用最明确的方案来解决。