DIYgod / RSSHub

🧡 Everything is RSSible
https://docs.rsshub.app
MIT License
32.14k stars 7.14k forks source link

知乎用户文章路由报错 #13081

Closed gjhuai closed 1 year ago

gjhuai commented 1 year ago

路由地址

/zhihu/posts/:usertype/:id

完整路由地址

/zhihu/posts/people/frederchen

相关文档

https://docs.rsshub.app/routes/social-media#zhi-hu-yong-hu-wen-zhang

预期是什么?

正常显示

实际发生了什么?

Looks like something went wrong Route requested: /zhihu/posts/people/frederchen Error message: this route is empty, please check the original site or create an issue Helpful Information to provide when opening issue: Path: /zhihu/posts/people/frederchen Node version: v18.17.1 Git Hash: 6422474

部署

RSSHub 演示 (https://rsshub.app)

部署相关信息

No response

额外信息

Looks like something went wrong
Route requested: /zhihu/posts/people/frederchen
Error message: this route is empty, please check the original site or create an issue
Helpful Information to provide when opening issue:
Path: /zhihu/posts/people/frederchen
Node version: v18.17.1
Git Hash: 6422474

这不是重复的 issue

github-actions[bot] commented 1 year ago
Searching for maintainers:

To maintainers: if you are not willing to be disturbed, list your username in scripts/workflow/test-issue/call-maintainer.js. In this way, your username will be wrapped in an inline code block when tagged so you will not be notified.

如果所有路由都无法匹配,issue 将会被自动关闭。如果 issue 和路由无关,请使用 NOROUTE 关键词,或者留下评论。我们会重新审核。 If all routes can not be found, the issue will be closed automatically. Please use NOROUTE for a route-irrelevant issue or leave a comment if it is a mistake.

TonyRL commented 1 year ago

/test

/zhihu/posts/people/frederchen
github-actions[bot] commented 1 year ago

Successfully generated as following:

http://localhost:1200/zhihu/posts/people/frederchen - Success ✔️ ```rss <![CDATA[Freder 的知乎文章]]> https://www.zhihu.com/people/frederchen/posts RSSHub i@diygod.me (DIYgod) zh-cn Sun, 20 Aug 2023 10:33:53 GMT 5 <![CDATA[高效率检测算法研究:高效训练CenterNet]]> 检测任务在保持精度的情况下,常关心两个指标:推理速度和训练效率。本文的“高效率检测算法”是指这两个指标都优秀的检测算法。如果不考虑网络蒸馏,优化推理速度更多需要考虑从网络架构入手,这方面目前已经较稳定。而优化训练效率则关心不降低精度的情况下减少网络所需的训练时间,往往从训练参数和训练方法入手,也存在稳定的方案但针对具体框架会有调整。

下面将分为两部分。第一部分介绍目标检测框架的历史来说明centernet检测框架推理速度的优越性,第二部分则根据TTFNet调优centernet来找到可复制地提升训练效率的trick。

推理速度优化

迄今为止,目标检测框架已经十分成熟。从网络结构上,我们可以将大多数的检测器划分为backbone、neck和head三部分。自SSD工作提出,多级检测成为了检测框架的主流,通过将不同大小的目标分配到不同尺度的特征图上,从而更好地去检测不同尺度的物体。但是这种多级检测架构阻碍了检测框架进一步提升推理速度,近年有工作[1,2,3]尝试使用单级检测架构。

如今,单级检测架构慢慢被大家认可。事实上,单级检测架构在YOLOv1[4]和YOLOv2[5]就已经出现。但正因为仅依靠backbone输出最后的特征图来检测太过于粗糙,会严重影响小物体的性能,这才使得检测框架走上“多级检测”的路。2018的CornerNet[1]和2019的CenterNet[2]以及follow-ups则换了一个角度考虑特征图粗糙的问题:融合多级特征图并输出高分辨率特征图似乎就能解决该问题,而无需采用多级检测。实验证明这样做有益。

类似于CenterNet的单级检测方法需要大分辨特征图和多级特征融合。尽管head被简化,但neck仍较为复杂。2020的DeTR[6]、2021的YOLOF[3]则没有采用多级特征融合,直接使用backbone的最后一层输出(C5)来进行检测获得理想结果。DeTR使用具有全局感受野的Self-Attention作为neck,YOLOF则提出了DilatedEncoder融合特征的感受野。所以从网络结构上来看,YOLOv1和YOLOv2失败的原因是其backbone输出特征所包含的尺度有限。

DeTR和YOLOF通过设计更好的neck来缓解backbone输出特征的局限,但这种增加neck复杂度的做法导致检测器并没有提速。另一方面,我们很容易意识到更好的方法是给backbone输出特征更大的感受野。kaiming在2022年开始尝试用Transformer[8,9]作为backbone来探索这个猜想,有不错的结果。[8]验证引入FPN更好,[9]则表明使用simple feature pyramid即可,侧面说明了增大感受野后优化困难的问题。这个问题在DeTR和YOLOF都有体现,他们及其follow-ups给出了一些解决方案。

但增大backbone感受野的最大问题在于现有方法(MHSA、large Conv.)严重影响推理速度,这与我们优化推理速度的初衷相悖,推理加速的下一步应该会解决这个问题。

训练效率优化

训练效率优化必须基于上述某框架,探索的是如何让模型更快更好收敛。目前的标准配置2x需要训练24 epoch,在流行架构上存在实现,如FCOS[10]、CenterNet++[11]、YOLOF[7]、TTFNet[12]。

可能因为这些trick不足以写一篇论文,很少有框架详细消融有效的修改。所以我们自己做了消融实验来说明该问题,并选择了目前最有效且稳定的CenterNet[2]来探究高效训练问题,其对应的论文就是TTFNet[12]。

对比TTFNet和CenterNet的训练时间和精度

实验采用resnet18进行对比,完全保留了两者的训练参数,在4卡P40上训练VOC,CenterNet的训练时间是TTFNet的5.8倍,大大节省了训练时间。

TTFNet的trick

TTFNet指出提升训练效率实质上是有效地增大学习率,而方法是为bbox wh的预测引入更合理的Loss权重并把创新点总结为增大训练mini-batch size。整篇论文具有非常强的迷惑性,其使用了椭圆高斯,相比于centernet的圆形高斯直觉上更合理,所以大多数人会认为椭圆是快速收敛的原因,但实际上高斯才提高了收敛速度。为wh的预测引入高斯权重,会让靠近center point的wh预测优化得更好,这能更好地匹配heatmap的预测目标,从而更好地优化。

对比如上图所示,可以看到在bbox比值接近1时,两种方法的区别并不大。我们的实验也证明了无论椭圆还是圆,使用高斯平滑wh target就能加快收敛速度。除了上述这一点,剩下的工作就是调整训练参数。我们总结了以下5点:

  1. WarmUp:微调必备
  2. MultiStepLR:训练后期衰减学习率能提升模型精度
  3. Bias lr *= 2,Bias wd = 0:a Caffe-style common practice
  4. 对于anchor-free,wh head需要乘以缩放因子scale(=16):加快收敛
  5. 对于centernet,offset和wh的回归Loss应该乘以高斯权重:配合heatmap分支

References

]]>
Thu, 07 Jul 2022 10:11:59 GMT http://zhuanlan.zhihu.com/p/538871864 http://zhuanlan.zhihu.com/p/538871864
<![CDATA[面试知识点 - 卷积 (Conv.)]]> 1. 卷积的定义

图像滤波矩阵(滤波器、卷积核)做内积的操作就是卷积。单次卷积操作就是对应位置的两个元素相乘,之后所有乘积相加。

在卷积神经网络中,通过卷积操作,可以提取图片中的特征低层的卷积层可以提取到图片的一些边缘、线条、角等特征,高层的卷积能够从低层的卷积层中学到更复杂的特征,从而利用提取到的特征,可以进行分类、目标检测等任务。高层的复杂特征具体代表啥,谁也不知道。

训练卷积网络是寻找某种模式匹配的过程。一个卷积核有且只有一种匹配模式,其实质是计算 N*N 的矩阵块与某一模式块的相似度。卷积网络的可视化解释_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

1. YJango的卷积神经网络——介绍
2. 深度学习之神经网络的结构 Part 1 ver 2.0

2. 卷积的特点

卷积最主要的特点就是局部感知权值共享

局部感知:卷积的大小远远小于图像,只需要感知图像中的局部信息,然后在更高层次进行信息组合理论上就可以得到全局信息,减少了计算量

权值共享:使用同一模板(相同的卷积核、参数)按滑动窗口的方式去提取特征,参数可以共享。这个可以带来平移不变性,而且一个卷积核可以只干一件事,只提取某一个特征。

头重脚轻:越靠近输入层,参数数量越少,越靠近输出层,参数数量越多,呈现出一个倒三角的形态,这就很好地避免了BP神经网络中反向传播的时候梯度损失得太快。

卷积能够平移变换使得其可以找出各种各样物体的特征。但卷积本身无法进行尺度变换、旋转变换。这两个问题一般通过制作多尺度和不同旋转角度的训练集缓和。同时也有少许相关工作——Spatial Transformer LayerVladlen Koltun: Beyond convolutional networks (June 2020)

1. Why rotation-invariant neural networks are not used in winners of the popular competitions?
2. 头重脚轻 - 参考CSDN

3. 卷积和全连接的区别

区别:输入输出不同,卷积输入输出都是NCHW格式,全连接输入输出都是NV结构;全连接网络两层之间的神经元是两两相连,卷积是部分相连。

全连接不适合做CV:1)参数量过多扩展性差;2)网络局限,梯度传播很难超过3层;3)没有利用像素之间的位置关系; 卷积适合做CV:局部连接、权值共享以及池化,减少参数、利用像素之间的关系。

4. 感受野

感受野是输出特征图上的像素点对于原图的映射区域的大小,特征图上的一个点相对于原图的大小。代表该像素点是由其感受野范围内的所有像素点得到的。

注意:

  • 实际上的有效感受野远远小于理论感受野,因为越中心的像素,被计算的次数更多,占的权重越大
  • anchor应该根据感受野来设置

计算

stride_i=stride_1 \times stride_2 \times ... \times stride_{(i−1)}\\ \mathrm{RF}_{i+1} = \mathrm{RF}_i + (f_{i+1} − 1) \times stride_i \times dilation_{i+1} \\

其中 \mathrm{RF}_i 为第i层感受野,f_{(i+1)} 为第i+1层卷积核大小,dilation_{(i+1)}为第i+1层空洞率。

最开始的输入层:i=0, \mathrm{RF}_0=1即第一层卷积之后的特征图感受野的大小等于卷积核的 size。

计算注意:

  • 第一层卷积层的输出特征图像素的感受野的大小等于滤波器的大小
  • 深层卷积层的感受野大小和它之前所有层的滤波器大小和步长有关系
  • 计算感受野大小时,忽略了图像边缘的影响,即不考虑padding的大小
1. 感受野 深度理解 - CSDN
2. 卷积神经网络中感受野的详细介绍 - CSDN

5. 反卷积(转置卷积)

反卷积是一种特殊的正向卷积,先按照一定的比例通过补padding来扩大输入图像的尺寸,接着旋转卷积核,再进行正向卷积。反卷积的参数不需要反向传播,而是行列转置之前卷积核的参数。

正向卷积:Y=WX 反卷积:X=W^T Y

但是,如果代入数字计算会发现,反卷积的操作只是恢复了矩阵X的尺寸大小,并不能恢复X的每个元素值。反卷积操作并不能还原出卷积之前的图片,只能还原出卷积之前图片的尺寸。

需要注意的是,反卷积本质是padding补0的卷积,而不是卷积的逆运算

反卷积的用途有:实现上采样;近似重构输入图像,卷积层可视化

1. 彻底搞懂CNN中的卷积和反卷积 - CSDN

6. 空间可分离卷积

空间可分离卷积就是将 n \times n的卷积分成n \times 11 \times n两步计算。

虽然空间可分离卷积可以节省计算成本,但一般情况下很少用到。主要原因之一是并非所有卷积核都可以分为两个较小的卷积核。如果用空间可分离卷积代替所有传统的卷积,现有优化器无法找到较优解(训练精度不高)。

1. 各种卷积层的理解(深度可分离卷积、分组卷积、扩张卷积、反卷积) - CSDN

7. 深度可分离卷积

深度可分离卷积是将卷积分为Depth-wise Convolution(逐深度卷积)和Point-wise Convolution(逐点1*1卷积)计算。

也用来减少卷积的参数。因此对于规模较小的模型,如果将2D卷积替换为深度可分离卷积,其模型大小可能会显著降低,模型的能力可能会变得不太理想,因此得到的模型可能是次优的。但如果使用得当,深度可分离卷积能在不牺牲模型性能的前提下显著提高效率。

逐深度卷积:将单个卷积核应用到单个通道并生成单个特征图

逐点1 \times 1卷积:跟普通卷积一样,不过使用的是1 \times 1的卷积,这样可以把深度卷积后的特征图的通道数增加

8. 分组卷积

设原通道为c_1,目标通道为c_2

将特征图在通道上拆分为gH \times W \times \frac{c_1}{g},对每个\frac{c_1}{g}都进行\frac{c_2}{g}3 \times 3 \times \frac{c_1}{g}的标准卷积,之后再拼接起来。相当于参数少了g倍。

如果g等于输入通道数,就变成了Depth-wise Convolution;如果g等于输入通道数,而且卷积大小等于输入图片大小,就变成了全局加权池化,权重是可学习的。

9. 可变形卷积

根据输入的图像,进行卷积来学一个偏移,学的是卷积核的偏移,然后进行正常卷积的时候,使用的偏移之后的像素。

1. 可变形卷积DCN - CSDN

10. 1 \times 1卷积

最初是在2014年GoogLeNet中首先应用 作用:

  1. 实现跨通道的交互和信息整合;
  2. 进行卷积核通道数(特征)的降维和升维;
  3. 可以实现与全连接层等价的效果
  4. 加入非线性(由于经过激活函数层)
  5. 通道融合
  6. 减少参数及计算量(MobileNet)

参考论文

主流论文概述学习目标
AlexNet(2010)深度学习的开端。卷积的定义与计算过程
VGG(2014)改进AlexNet。现在大量使用3x3卷积核的原因
GoogLeNet(2014)加宽网络层,提升精度。InceptionV1
InceptionV2V3V4(2015)分解卷积核,提升速度。卷积分解
ResNet(2015)经典,现在最广泛的网络。残差模块与Inception的异同
MobileNet(2017)分解Channel,提升速度。空间、深度可分离卷积
ShuffleNet、Xception(2017)改进MobileNet。Group Conv.与MobileNet的区别

]]>
Tue, 08 Mar 2022 05:35:56 GMT http://zhuanlan.zhihu.com/p/477558365 http://zhuanlan.zhihu.com/p/477558365
<![CDATA[论文笔记:Going deeper with Image Transformers]]> 该文(CaiT)由Facebook AI提供,与DeiT是姊妹篇。

Introduction

CaiT旨在为图像分类构建和优化更深层次的Vision Transformer。文章从Resnet引入中规中矩,贡献有二:

一是从一些现有方法引申出来的LayerScale。这个方法解决残差连接的问题,其本质是残差连接会放大方差,具体参考残差连接-苏剑林

二是发现Image Patches和Class Token的优化目标矛盾提出的Class-Attention Layer。将两者优化参数分离,从而避免了矛盾。

Method

Deeper image transformers with LayerScale

文章在做DeiT时发现:随着网络加深,精度不再提升。以“Going Deeper”作为Motivation,CaiT发现是残差连接部分出现了问题。Fixup, ReZero和SkipInit在残差块的输出上引入了可学习的标量权重\alpha_l,同时去除了和PreNorm和Warmup。具体如下式:

x_l' = x_l + \alpha_l SA(x_l) \\ x_{l+1} = x_l' + \alpha_l' FFN(x_l') \\

但是作者的实验使用这些方法即使微调参数也无法收敛。文章的经验是:去除Warmup和PreNorm是导致Fixup和T-Fixup中训练不稳定的原因。因此重新引入这两步,使Fixup和T-Fixup在DeiT模型中收敛,见下图(c)。

CaiT提出的LayerScale是每个残差块产生的向量的通道权重,而不是单个标量,见上图(d)。

Specializing layers for class attention

Class Token与Image Patch Token分离可以理解为简单的Encoder-Decoder结构。在Self-Attention阶段,Class Token不参与运算;而在Class-Attention阶段,Image Patch Token不再随着网络加深而改变。这个思路貌似很有道理,但是看消融实验,没啥用= =。可以从下图看到,精度基本和AveragePooling一致。

所以这篇应该重点学习LayerScale,而Class-Attention可以当作问题来考虑。

Experiment

实验部分,CaiT悄悄把Talking-Heads Attention加了进来作为SA的注意力模块(ClsA仍采用MHA),将DeiT-Small在Imagenet上的性能从79.9%提高到80.3%。THA点开扫一眼就觉得神了,第一次见这么随意的风格……其余细节推荐看原文。

Conclusion

在可视化部分,文章讲了两个观察成为精华:

  • 第一个类注意力层明确关注感兴趣的对象,对应于执行分类决策(正确或不正确)的图像的主要部分。在这一层中,不同的头部要么关注对象的相同部分,要么关注对象的互补部分。这对于瀑布图像尤其明显;
  • 第二个类注意力层似乎更多地关注上下文,或者至少更全局地关注图像。

Reference

  1. Going deeper with Image Transformers - arxiv
  2. CaiT - github
  3. 残差连接-苏剑林
  4. Talking-Heads Attention - arxiv
  5. 突破瓶颈,打造更强大的Transformer - 苏剑林
]]>
Sat, 09 Oct 2021 14:20:22 GMT http://zhuanlan.zhihu.com/p/419666958 http://zhuanlan.zhihu.com/p/419666958
<![CDATA[论文笔记:Not All Images are Worth 16x16 Words: Dynamic Vision Transformers with Adaptive Sequence Length]]> 本文由清华大学与华为合作,NeurIPS2021。

Introduction

2020年Transformer在图像识别取得成功后,各种相关(类ViT)方法喷涌而出。大家通常将一个2D图像分成固定数量的Patch,每个Patch都被视为一个Token。一般,随着Patch的数量增加,预测精度提升,同时计算成本也会增加。为了在精度与速度之间取得平衡,根据经验将Token的大小设为16*16像素。

文章观察到:存在相当多的“简单”图像样本可以用较少的Token准确预测,而只有小部分“难”样本需要更精细的表示。受这个观察的启发,文章提出了一种动态的Vision Transformer来为每个输入图像以自适应方式顺序激活。它通过多个级联的具有越来越多Token的Transformer来实现;从粗粒度预测开始,至产生足够可信的预测终止推理。同时进一步设计了级联Transformer之间的有效特征重用和关系重用机制,以减少冗余计算。

ImageNet、CIFAR-10和CIFAR-100上的大量实证结果表明,文章方法在理论计算效率和实际速度方面都明显优于Baseline。

Method

Overview

细看结构图,我们可以找出三个感兴趣的模块:ExitFeature \space ReuseRelationship \space Reuse

Exit Module

文章使用的方法很简单,就是输出概率大于阈值(比如0.8)后就输出结果。但由于文章切入点是:对于每张图片,由于其复杂程度不同,对应的最好的patch切割数量也不相同。那么我们应该首先寻找对于当前图片的最佳切割数量(方式)。

这个最佳切割数量值很难估计。所以运用“Simple is best”原则:从最少的数量开始切割,有可信结果产生就认为当前切割数量是图像的最佳切割数量。

但是不是真的“Simple is best”,个人认为值得怀疑。这里又很难推导证明或实验证明,所以这里可能是较好的解决方法。

Reuse Module

我们知道ViT的输入和输出是等维度的,所以刚看到结构图时就很好奇这两个Reuse module是如何接到下一层的。细看才知道Upsample……(是我菜了。

Reuse Module设计的初衷是下游模型应该在先前获得的深度特征的基础上学习,而不是从头开始提取特征,直觉上会提高模型的效率。

Feature reuse

  1. 不同数量的Token如何对齐
    我们知道ViT会通过Classification Token预测最后的结果,而其他的Token直接丢弃。在DVT中,我们可以将其他Token当作对应patch的feature,送入下游模型。由于patch个数不对应,先按照图片对应位置排成n个channel的图像,后Resize成下游模型的Token数,达到Upsample的目的。
  2. Classification Token为什么不作为feature送入下游模型
    实验证明(Table 5)加入Classification Token并不会有明显提升,甚至有过拟合风险。

3. Feature Token为什么每一层都要输入
文章对只在第一层使用feature reuse和全部层都使用feature reuse作对比,下表为实验结果。

Relationship reuse

通常,模型需要在每一层学习一组注意力关系矩阵来描述Token之间的关系(q \times k)。文章认为这些学习到的关系也能够被重用以促进下游 Transformer 的学习。具体做法就是把每一层的Attention concat起来,通过MLP压缩、Upsample对齐、矩阵相加实现先验的输入。

其中,上采样需要像feature reuse的上采样一样将Attention先按照图片对应位置排成n个channel的图像然后Resize,而不能直接Upsample。操作如下图:

Relationship Reuse的消融实验结果如表 6 所示:

  1. 仅重用来自相应上游层的注意力;
  2. 仅重用来自最终上游层的注意力;
  3. 用rl(·)中的线性层替换MLP;
  4. 采用naive upsample操作;

结果表明,使每个下游层能够灵活地重用所有上游注意力更有益。普通的上采样显着损害了性能。

Experiment

Speed Experiment

文章设计DVT主要就是希望加快推理速度,实验对比发现整体速度确实得到了成倍的提升。

Accuracy Experiment

由于DVT使用了T2T-ViT作为backbone,所以文章仅将T2T-ViT和DVT作比较。可以看出精度没有提升,但能与backbone保持一致。

Effect of Reuse Module

Conclusion

从下图(左)可以看出Easy Image往往是目标大而背景单一的图片,随着目标变小、背景边复杂,图片的分类难度会逐渐上升,从而需要更细粒度的Patch来预测。

Reference

  1. arxiv
  2. github
]]>
Mon, 21 Jun 2021 01:56:29 GMT http://zhuanlan.zhihu.com/p/382489231 http://zhuanlan.zhihu.com/p/382489231
<![CDATA[论文笔记:A Local-to-Global Approach to Multi-modal Movie Scene Segmentation]]> 这篇是香港中文大学SenseTime Joint Lab的Anyi Rao在CVPR2020上发表的关于视频幕分割的论文。

这里先讲三个前置定义:

1. 帧:视频中单幅的静态图片。

2. 镜头:视频中像素差异不大的连续帧集。

3. 幕:视频中语义差异不大的连续帧集。

Introduction

论文的Motivation很有意思,从电影场景出发,表示视频幕分割是向视频语义理解迈出的关键一步。为了实现这一目标,论文自己制作了MovieScenes数据集,该数据集包含来自150部电影的21K带注释的场景片段。并提出了一个由局部到全局的baseline。

Method

这个任务是对视频在时间维度做类不可分的分割。它预期将视频中语义相似的连续帧分在同一幕(Scene)中。论文假设每帧都会有自己相应的语义(即每帧都应该属于某一幕),所以将label设置为一个分类序列,序列的每个点表示相应两帧之间是否为语义分割点。

LGSS\{[f_1, f_2, \cdots, f_n]\} = [o_1, o_2, \cdots, o_{n−1}], \text{where } o_i ∈ \{0, 1\} \\

由于同一镜头(Shot)下只会有一个语义,且镜头提取方法已经较为成熟,所以baseline先将帧提取为镜头来提高分割效率。

LGSS\{[s_1, s_2, \cdots, s_m]\} = [o_1, o_2, \cdots, o_{m−1}], \text{where } o_i ∈ \{0, 1\} \\

至此的理论应该都会认同。重点是论文如何设计并训练这个网络。

镜头预处理:从帧到镜头

视频通过LGSS生成幕之前,需要生成镜头(s_1,s_2,...,s_m)。论文使用了传统方法,检测帧间色彩和强度变化来粗略切出镜头(仅利用图片信息)。

同时,论文希望捕捉电影更丰富的语义信息,将每个镜头用位置(place)、演员(cast)、动作(action)和音频(audio)表示。这里每个镜头的特征提取都是采用的预训练模型,没有适配数据集训练。

  1. place特征:采用Places数据集上预训练的ResNet50。Places
  2. cast特征:采用CIM数据集上与训练的Faster-RCNN进行检测,采用PIPA数据集上预训练的ResNet50进行特征提取。CIM, PIPA
  3. action特征:采用AVA数据集上预训练的TSN。AVA, TSN
  4. audio特征:采用AVAActiveSpeaker数据集[20]上预训练的NaverNet [5]分离背景和声音,并用stft 分别以16KHz和512的窗口长度采样获取镜头特征,最后concat起来得到音频特征。AVAActiveSpeaker, NaverNet

镜头的幕尾预测:从镜头到幕

视频帧切出镜头并提取特征后,就做好了分割幕的所有准备工作。现在需要一个Magic网络实现下面的功能:

LGSS\{[s_1, s_2, \cdots, s_m]\} = [p_1, p_2, \cdots, p_{m−1}], \text{where } p_i ∈ \{0, 1\} \\

简单来说,就是希望输入提取出的连续的镜头特征,得到每连续两镜头间隔是幕结尾的概率。

特征压缩:限制输入规模

由于镜头序列可能很长而计算机资源有限,作者设计BNet(Boundary Network)。BNet以2w_b个镜头特征作为输入并输出2w_b幕的特征b_i

\begin{equation} \begin{split} b_i &= BNet([s_{i−(w_b−1)} , \cdots , s_{i+w_b}]) \space \text{(window size 2w_b)} \\ &=      \left[         \begin{array}{ccc}         B_d([s_{i−(w_b-1)}, \cdots, s_i], [s_{i+1}, \cdots, s_{i+w_b}])\\         B_r([s_{i−(w_b-1)}, \cdots, s_i, \space \space s_{i+1}, \cdots, s_{i+w_b}])     \end{array}     \right ] \end{split} \end{equation} \\

如下图所示,BNet由B_dB_r两个分支组成:B_d捕捉镜头前后的差异,由两个时间卷积层建模,每个时间卷积层分别在边界之前和之后嵌入镜头,然后进行内积运算以计算它们的差异;B_r寻找镜头之间的关系,由时间卷积层和最大池化实现。(这里为什么这么实现?)

全局优化:Local-to-Global

使用BNet压缩局部特征后即可使用LSTM模块来做sequence-to-sequence的全局优化。这里直接送入模块即可:

p_1 = LSTM(b_1) \\ \cdots \\ p_{n-1} = LSTM(b_{n-1}) \\

结构如下图所示:

然后作者在4.4节讨论"电影级别的全局最佳分组",大意是过LSTM模块的分割结果还不够全局,所以再把每一幕的特征送进模型再来一遍???那之前为了减少内存设计BNet不是白搞了……得到\overline{o}_i的意义就是获得一个假label?……牛。
最重要的是在github中没找到这个G的实现,好像并没有处理。没看懂……这块推荐再看看论文,希望能有人告诉我。
The segmentation result \overline{o}_i obtained by the segment-level model T is not good enough, since it only considers the local information over w_t shots while ignoring the global contextual information over the whole movie. In order to capture the global structure, we develop a global optimal model G to take movie-level context into consideration. It takes the shot representations s i and the coarse prediction \overline{o}_i as inputs and make the final decision o_i as follows,
[o_1, \cdots, o_{n−1}] = G([s_1, \cdots, s_n], [\overline{o}_1, \cdots, \overline{o}_{n−1}]) \\

Result

参数设置

  1. dataset:train:val:test=10:2:3
  2. loss:CrossEntropyLoss,由于label比例为9:1,损失设为了1:9
  3. optimizer: Adam, lr=0.01,
  4. scheduler:MultiStepLR, milestones=[15]
  5. epoch:30

评价指标

  1. AP
  2. MIOU
  3. Recall

实验结果

MethodAP(↑)M_{iou}(↑)Recall(↑)Recall@3s (↑)
The framework with shot boundary modeling using BNet raises the performance from 24.3 (Multi-Semantics) to 42.2 (MultiSemantics+BNet) (73.7% relatively) which suggests that in the scene segmentation task, modeling shot boundary directly is useful.

作者说BNet提升很大,表明在场景分割任务中,直接对镜头边界进行建模非常有用。我反倒觉得是网络没有网络作用有限。

关于特征选择

关于w_b的选择

Conclusion

  1. lgss只是把多模态特征简单的分别提取概率加权相加,可以考虑在input处联合。
  2. 正如论文所说,感觉各shot提取特征以后,从local到global的过程还可以更global一些。
  3. 幕不能存在重叠?

Reference

  1. Paper PDF
  2. Paper Talk
]]>
Wed, 12 May 2021 08:13:39 GMT http://zhuanlan.zhihu.com/p/371728924 http://zhuanlan.zhihu.com/p/371728924
<![CDATA[Java调用TensorFlow]]> 前述

最近在做一个视觉方面的Demo。坑当然是多到不行,想到这都是了解生态的一个过程,也就不那么烦躁。我们的模型训练部分往往是用Python写Keras或者直接上TensorFlow,然后得到model。但部署这件事还没听说直接用Python就能解决,大多需要别的工具。

第一种方式是通过网络,以服务器、客户端的形式实现。这时候可以写个简单的Flask接口就可以实现建议的模型部署,稍复杂、专业一些就可以用到TF Serving之类的专门的部署工具。很容易理解,这种方式使用模型的服务必须联网,由于是一些视觉方面的应用,对网络的要求可能还比较高。

第二种方式是本地化部署,将模型打包在App中直接在本地调用。App一般情况下都不是Python开发,更可能是JS、Swift、C++、Java等其他语言(TF支持JS、Swift、C/C++、Java、Go等等)。这种方式的最大缺陷就是受到设备计算资源的限制,但在我的Demo中勉强能够使用。最终也是选择了这种方式。

部署原理

详细来说,这篇是使用Java调用Python训练的模型。首先第一个坑,Java目前好像只支持TensorFlow1,所以Python训练也不能使用TensorFlow2。简单来说,这个部署过程就是将h5、checkpoint格式的model转成pb格式的model。在Java中只能读取pb格式的model。

格式转换工具

转换格式一般采用Python脚本,推荐使用pyenv保持同Java TF版本一致的Python版本及TF版本(曾因版本不同出现过莫名的问题)。关于TF的安装

如果使用Keras,推荐使用keras_to_tensorflow

# ReadMe中有使用方法
# python keras_to_tensorflow.py
#     --input_model="path/to/keras/model.h5"
#     --input_model_json="path/to/keras/model.json"
#     --output_model="path/to/save/model.pb"
# h5文件通过save_weights保存
model.save_weights('model.h5')
# json文件通过to_json得到
with open("model.json", "w") as json_file:
  json_file.write(model.to_json())

如果使用TensorFlow,可以使用tf-ckpt-2-pb

# ReadMe中的Usage
# python convert.py
#     --checkpoint "path/to/tf/ckpt_weight"
#     --model "path/to/tf/ckpt_weight/model.ckpt.meta"
#     --out-path "path/to/save/out.pb"
# checkpoint是ckpt文件夹
tf.train.Saver().save(session, path)
# mdoel是ckpt文件夹中的meta文件

找出model的input和output

Java调用TF model需要知道Input layer name和Output layer name,这时候可能需要使用tensorboard工具,自己去看网络结构。

import tensorflow as tf
from tensorflow.summary import FileWriter
sess = tf.Session()
tf.train.import_meta_graph("path/to/tf/ckpt_weight/model.ckpt.meta")
FileWriter("__tb", sess.graph)
# after run python script,
# run cmd: tensorboard --logdir __tb

只出不入的大概就是Input layer,只入不出的很可能就是Output layer。唯一需要注意的是将名称写全,比如有仅一层的名字input,也有几层的名字generator/MODEL/outLayer

Java调用TensorFlow的方法

得到pb文件、Input layer name和Output layer name就只差写Java代码调用啦。安装 Java 版 TensorFlow

这时候按照官方教程走,应该不会出什么问题。教程中现在用的是1.14.0的版本,所以python中也最好用1.14.0版本的TF。版本问题前面就已经提到过,不多说。

<dependency>
  <groupId>org.tensorflow</groupId>
  <artifactId>tensorflow</artifactId>
  <version>1.14.0</version>
</dependency>

简易的载入模型和预测函数及使用:

# TensorFlowUtils.java
public final class TensorFlowUtils {
    public static Session loadModel(String modelPath, Class<?> cls) {
        try {
            Graph graph = new Graph();
            Session session = new Session(graph);
            graph.importGraphDef(IOUtils.toByteArray(cls.getResourceAsStream(modelPath)));
            return session;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    public static void closeModel(Session session) {
        session.close();
    }
    public static Tensor<Float> predict(Tensor<Float> input, Session session, String inputName, String outputName) {
        return session.runner().feed(inputName, input).fetch(outputName).run().get(0).expect(Float.class);
    }
}
# Main.java
public class Main {
    public static void main(String[] args) {
       Session session = TensorFlowUtils.loadModel("model/path", getClass());
       // float[][][][] originInput = may be an image, or others
       Tensor<Float> input = Tensor.create(originInput, Float.class);
       Tensor<Float> output = TensorFlowUtils.predict(input, session,
                "<your input name>", "<your output name>");
       float[][] result = output.copyTo(new float[1][2]);
       System.out.println(result[0]);
       TensorFlowUtils.closeModel(session);
    }
}
]]>
Sun, 06 Sep 2020 01:55:49 GMT http://zhuanlan.zhihu.com/p/219744718 http://zhuanlan.zhihu.com/p/219744718
<![CDATA[Share Analysis]]> Share是自己做股票的小工具(目前只支持深沪两市)。Share Analysis试图使用机器学习方法挖掘股票价值。

随想

在尝试进入股票市场后,我心中一直有个很大的疑问:股票价格究竟与什么有关?

有人说公司的资产价值(不仅仅是总资产,还包括偿债能力、盈利能力、成长能力等等)影响股价。但财务报表一季度发一次,在此期间也不是按照一个方向运动,很明显不能作为交易指标。如果说都是幕后消息造成的,那么所有散户都是韭菜,我认为这并不合理。所以得出“资产价值只要不超过某一范围就应该被认为合理”的结论。

有人说公司分红影响股价。我觉得这个比较靠谱,为此还专门写了一篇博客——从公司分红看股票价值,最后得出有一点点影响的结论。从博文中可以很明显的看出,股票市场的利润大头还是卖出和买入差价。尤其在国内市场,分红由公司自己来决定,过去分红多不代表未来分红多,甚至未来都不一定有分红。所以只能说过去有分红的公司风险较小,但作为交易指标有点牵强。

有人说市场(也就是大环境,比如国内经济因素)能带动所有股票齐涨齐跌。也就是在说:市场齐涨叫牛市,市场齐跌叫熊市,市场涨跌互现叫震荡市。市场所处周期很难判断,齐跌完出现反弹应该是认为牛市还是震荡市完全靠个人经验。如果无法分辨市场当时所处周期,那么所处周期只能是后验的。

有人说公司所在地域、行业影响股价。这个观念和上一条的市场论相似,甚至应该认为就是细分以后的市场论。使用这个方法需要有一些眼界,我的经验是从生活中发现价值,比如AirPods刚出来的时候搜索发现“立讯精密”是生产商果断买入、疫情期间医药行业公司会鸡犬升天。这是目前实践下来比较有用的观点。另外,该观点有一个很大的缺陷在于市场存在错配情况,导致市场有时不会立马验证判断的正确性,这样就无法判断究竟是市场错了还是自己错了。也由于这个缺陷,很多人从做交易转向哲学……

还有人说市场大众的心理因素影响股价。这是在说:市场买方多于卖方就涨,反之就跌。我认为这是最接近真实股价的逻辑。问题在于如何才能知道大众对市场的看法,为此引申出了技术分析——企图从历史交易数据中发现大众对市场的看法。

基于以上观念,目前自己的交易大致流程:公司资产不存在大问题、历史上有稳定分红就可入选观察范围;个人判断公司可能增值就开始关注股价走势;大众开始对市场看多(也就是技术分析层面看多)才决定入场。

说回Share Analysis。当前的Share Analysis是不完善的项目。只使用各平台分析数据来做排序,人为盯盘还是必不可少的步骤。使用各平台的分析数据是因为我发现家里人买股票非常迷信这种“高级”指标,而这一指标并不在我的系统范围内,为了简化流程,稍做了一些编码工作来实现局部自动化。未来可能会加入更多的新功能。

原理

各平台数据一般是在交易日晚上更新,并且无法访问历史数据,所以写爬虫并将数据保存在本地是必须的流程。爬虫项目目前不准备开源(能用但是没写好),我写了简单的后台接口(在项目中设为了默认数据请求源)希望不会被爆破。

各平台的数据基本都是分数,可直接作为特征使用。Label做了一些尝试,不知道能不能给出更好的额答案(有想法欢迎讨论)。由于国内交易需要T+1,选择了“第一天的数据使用第三天的收盘价减去第二天的开盘价大于0标1否则标0”作为Label。

在训练的时候使用前三十天的数据制作模型,预测后一天的情况。测试时需要注意,倒数第二天不能加入训练集,因为预测时也是没有这一天的,如果加入可能造成测试、预测结果不一致的情况,甚至某种程度上算数据穿越。

安装

  1. 安装Python3
  2. 安装LightGBM模型包
  3. 安装Python库
python3 -m pip install requests, numpy, pandas, sklearn, lightgbm, matplotlib, fire

功能及使用

# 打开cmd输入, n为训练数据日期跨度(默认30),end为最后一天日期(默认today)
python3 -m share_analysis test --n 30 --end \"yyyymmdd\" # 测试: end 为pred lebel的日期
python3 -m share_analysis pred --n 30 --end \"yyyymmdd\" # 预测: end 为pred feature的日期

同时会对数据做一些缓存,目录在utils.py中,对应代码:

TMP_PATH = os.path.join(tempfile.gettempdir(), 'share_analysis')

有趣的链接

]]>
Thu, 09 Jul 2020 10:41:59 GMT http://zhuanlan.zhihu.com/p/158158481 http://zhuanlan.zhihu.com/p/158158481
<![CDATA[如何在Windows环境下写一个控制台文本编辑器]]> 从高中毕业就开始研究Editor,想写一个自己的文本编辑器。 大一时不了解其他东西就想用控制台写一个,留下了这么一个不完备的多行输入函数。

1. 准备

在尝试实现这个函数的过程中,我试过printf()系列,也试过getchar()系列,还有getch()系列……后来发现回车入栈函数并不能满足多行文本输入的需求,最后找到了_getch()_putch()。由于char类型不能容纳中文(2个字节),选择了wchar_t作为储存字符的类型,与之相匹配的函数是void _putwch(wchar_t c)wchar_t _getwch()

现在可以很容易实现输入一个字符并输出:

wchar_t ch = _getwch();
_putwch(ch);

2. 架构

2.1. 基本数据结构

  • Edit box config
struct EditBoxConfig {
  char *filename;
  short cursor_x, cursor_y;
  short screen_rows, screen_cols;
  struct Word *head_word;
  // Head_word has no characters(WEOF) which follows by the text.
};
  • Word
    我使用了双向链表来储存输入字符,但也可以尝试char *这样的数组配合mallocrealloc来写,理论上是更好的选择。
struct Word {
  wchar_t ch;
  struct Word *last;
  struct Word *next;
};

2.2. 输入

  • 输入函数需要正确处理输入的内容:
wchar_t ch;
switch (ch = _getwch()) {
    // You can find the number of these characters on the Internet.
    case ARROW_KEY:
        move_pointer(_getwch());
        break;
  case BACKSPACE:
        delete_word();
        break;
  case ...:
        /* You can do what you want.*/
        break;
  default: /* Word and enter. */
        insert_word(ch);
        break;
}
  • 在输入过程中需要定义一个present_word在储存当前指向的字符。我们可以将它看作虚拟光标。

2.3. 输出

  • 输出需要在正确的位置渲染出字符:
// This is print word head.
// If the text is beyond the screen,
// it will not be the head of text.
struct Word *word = word_head;
// Print line by line.
for (i = 0; i < editor.screen_rows - 1; i++) {
    // If current Word is a word, print it on screen.
    // Else if current Word is newline character, print space after this position.
    print_word_and_update_cursor();
}
move_cursor_to(editor.cursor_x, editor.cursor_y);

2.4. 输入、输出函数联系

while (1) {
    refresh_screen();
    process_keypress();
}

3. 想说的话~~(吐槽~~=。=

从开始这个项目到现在很久很久了,最主要的问题是我很少找到可以借鉴的资料,导致战线超级长。这也从另一方面反映这东西的奇葩(=。=)

从一开始的想写一个自己的文本编辑器,到和编辑框杠上,有很多可以回忆的往事。所以虽然这东西简单,但我很想记录下来,它对我来说是非常重要的回忆。记得几次寒假一两周不出门,每天十几小时花在上面都是辛酸泪。

另外,我的Demo还有很多功能没有实现。比如:翻页、一些常用键功能……短期内不会再弄这东西了。

4. 参考资料

  1. My demo in github
  2. Kilo
  3. Kilo解析
]]>
Sat, 13 Jun 2020 14:40:02 GMT http://zhuanlan.zhihu.com/p/148145158 http://zhuanlan.zhihu.com/p/148145158
<![CDATA[如何开始一场数据分析比赛:以2018科大讯飞AI营销算法大赛为例]]> 说来惭愧,这篇文章是比赛时就开始准备,拖到答辩后才完成。

写这篇文章的缘由是开始做2018科大讯飞AI营销算法大赛, 被好友问到如何入门数据分析,由于我自己算是零基础边比赛边学,一时间不知道如何回答。毕竟“输的时候说什么都是错的”。最后取得第四名的成绩外出答辩受宠若惊,也给了我一点底气聊一聊自己的想法。同时,我必须强调这次比赛多亏了各位大佬的Baseline,否则不可能获得如此成绩。秉承继承分享精神的心情写下一点拙见。

由于我所属的队伍对于特征并无过多交流,以至于最后答辩的时候出现了不知道对方做了什么的尴尬情况。所以这里只能谈谈我自己的一点思路。大多观点借鉴这篇转载文章

开头的胡言乱语

我一直认为实战是最好的入门材料,数据分析亦如此。所以不要害怕面对比赛一行代码都敲不出来。一般情况下会有 baseline 作为参考,照着 baseline 一行一行敲,查每一个函数的意思、用法,慢慢的学会怎么使用数据分析工具。然后再去考虑研究每一个函数内部的原理。

先让程序跑起来

我们可以借鉴 baseline 中的模型,将所有应该导入的包装好以后以最快的速度跑出一个答案。

数据分析的一般步骤是读数据、填充缺失值、提取特征、模型的训练及预测。但要以最快的速度将模型跑起来就将所有 object 类型的特征 drop 掉。不要考虑提取特征。

我们可以通过 dtypes 查看一个DataFrame 里面的数据类型,通过 isnull 查看 DataFrame 里数据为空的情况。

import os
import pandas as pd
test = pd.read_csv(os.path.join('Data', 'round1_iflyad_test_feature.txt'), sep='\t')
print(test.dtypes)
NAs = test.isnull().sum()
print(NAs[NAs > 0])

运行以后我们可以看到:

instance_id                int64
time                       int64
...(太长省略)
app_paid                    bool
advert_name               object
dtype: object
user_tags      13232
make            4126
...(太长省略)
f_channel      36637
app_id            31
dtype: int64

有这个结果我们就能知道哪些是我们应该删除的特征(比如 advert_name),哪些是我们应该填充的缺失值。现在就可以进行数据预处理:

import os
import pandas as pd
def show_NaN(df):
    NAs = df.isnull().sum()
    print(NAs[NAs > 0])
def fill_missings(df):
    df['make'] = df['make'].fillna('-1')
    df['model'] = df['model'].fillna('-1')
    df['osv'] = df['osv'].fillna('-1')
    df['app_cate_id'] = df['app_cate_id'].fillna(-1)
    df['app_id'] = df['app_id'].fillna(-1)
    df['click'] = df['click'].fillna(-1)
    df['user_tags'] = df['user_tags'].fillna('-1')
    df['f_channel'] = df['f_channel'].fillna('-1')
    return df
print('Begin to read database...')
train = pd.read_csv(os.path.join('Data','round1_iflyad_train.txt'), sep='\t')
test = pd.read_csv(os.path.join('Data', 'round1_iflyad_test_feature.txt'), sep='\t')
all_data = pd.concat([train, test], sort=False)
print('Begin to do pretreatment...')
all_data = fill_missings(all_data)
numeric_feats = all_data.dtypes[all_data.dtypes == 'object'].index
all_data = all_data.drop(numeric_feats, axis=1)
print('Begin to save database...')
print(all_data.dtypes)
print(all_data.shape)
show_NaN(all_data)
all_data[:train.shape[0]].to_csv(os.path.join('Data', 'train.csv'), index=False)
all_data[train.shape[0]:].to_csv(os.path.join('Data', 'test.csv'), index=False)

PS:我将所有数据文件放入了当前路径的 Data 文件夹中,你可以适应自己的文件路径。

选择模型这方面我也知之甚少,是以后应该重点补足的点。但开始一个比赛模型复制 baseline ,大方向不会错。

import os
import time
import datetime
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
def lgb_train(X_train, X_test, y):
    import lightgbm as lgb
    from sklearn.cross_validation import StratifiedKFold
    X_loc_train = X_train.values
    y_loc_train = y.values
    X_loc_test = X_test.values
    res = X_test.loc[:, ['instance_id']]
    model = lgb.LGBMClassifier(
        boosting_type='gbdt', num_leaves=48, max_depth=-1, learning_rate=0.05,
        n_estimators=2000, max_bin=425, subsample_for_bin=50000, objective='binary',
        min_split_gain=0,min_child_weight=5, min_child_samples=10, subsample=0.8,
        subsample_freq=1, colsample_bytree=1, reg_alpha=3, reg_lambda=5, seed=1000,
        n_jobs=10, silent=True)
    # 五折交叉训练,构造五个模型
    baseloss = []
    for i, (train_index, test_index) in enumerate(list( \
        StratifiedKFold(y_loc_train, n_folds=5, shuffle=True, random_state=1024))):
        print('---Fold', i)
        lgb_model = model.fit(
            X_loc_train[train_index], y_loc_train[train_index],
            eval_names =['train', 'valid'],
            eval_metric='logloss',
            eval_set=[
                (X_loc_train[train_index], y_loc_train[train_index]),
                (X_loc_train[test_index], y_loc_train[test_index])
            ],
            early_stopping_rounds=100
        )
        baseloss.append(lgb_model.best_score_['valid']['binary_logloss'])
        res['prob_%s' % str(i)] = lgb_model.predict_proba(
            X_loc_test, num_iteration=lgb_model.best_iteration_)[:, 1]
        print('mean:', res['prob_%s' % str(i)].mean())
    res['predicted_score'] = res[['prob_' + str(i) for i in range(5)]].mean(axis=1)
    print('logloss:', baseloss, np.mean(baseloss))
    print('mean:', res['predicted_score'].mean())
    now = datetime.datetime.now().strftime('%m-%d-%H-%M')
    res[['instance_id', 'predicted_score']].to_csv(
        os.path.join('Submit', 'lgb_%s.csv' % now), index=False)
print('Begin to read csv...')
train = pd.read_csv(os.path.join('Data', 'train.csv'))
test = pd.read_csv(os.path.join('Data', 'test.csv'))
all_data = pd.concat([train, test], sort=False)
print('Begin to plot database...')
X_train = all_data[:train.shape[0]].drop(['click'], axis=1)
X_test = all_data[train.shape[0]:].drop(['click'], axis=1)
y = train['click']
print('Begin to train...')
lgb_train(X_train, X_test, y)

至于什么是LightGB、什么是N折交叉训练,以后再去弄明白。这样算是能跑出一个结果了。

提取特征

在有一个能跑通的模型基础上,我肤浅的谈谈特征工程。

首先我们看看每个特征里面有多少类值:

df = pd.read_csv(os.path.join('Data', 'round1_iflyad_train.txt'), sep = '\t')
print(df.nunique().sort_values())
app_paid                       1
creative_is_voicead            1
...
nnt                            6
creative_height               13
creative_width                20
app_cate_id                   22
advert_industry_inner         24
advert_name                   34
province                      35
advert_id                     38
creative_tp_dnf               40
campaign_id                   64
f_channel                     73
osv                          300
city                         333
...
instance_id              1001650
dtype: int64

creative_is_voiceadcreative_is_js类别为1的项就可以直接剔除掉;类似于creative_has_deeplink这样的二值特征直接使用问题不会太大;而creative_width这样的特征直接传入也是可以的。

仔细观察就会发现这些特征里面全是ID类离散特征。常理来说,ID类特征是不能直接导入模型的,这样的特征要么One-Hot、要么对其进行排序。我的特征都是围绕这两项展开。One-Hot比较简单而且易于理解,代表着当前类别对Label的影响,这里不过多叙述。

重点说说对ID排序。排序是希望不规则的ID与Label呈线性关系(即ID越大点击率越高之类),那么就这个问题来说广告点击率(CTR)是最为直接的排序方式。首先我们就可以对不同特征的CTR进行一些测验,比如考虑不同类型的广告点击率不同:

import matplotlib.pyplot as plt
df = pd.read_csv(os.path.join('Data', 'rou
```
TonyRL commented 1 year ago

It works.