<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ignorance.moontree</title>
    <description>Swimming in the ocean of coding</description>
    <link>https://moontree.github.io/</link>
    <atom:link href="https://moontree.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 18 Oct 2021 11:03:54 +0000</pubDate>
    <lastBuildDate>Mon, 18 Oct 2021 11:03:54 +0000</lastBuildDate>
    <generator>Jekyll v3.9.0</generator>
    
      <item>
        <title>Moving Percentile</title>
        <description>&lt;p&gt;事情的背景是这样子的，我们训练了一个id类为主要特征的模型，而且id的更新特别快，生命周期在3天左右，但是由于流程问题，模型只能天级更新。
模型给出一个预估分数，后续通过卡阈值来进行某些操作。上线后发现一个问题：每天的输出值分布变化较大，导致阈值不能卡固定值。&lt;/p&gt;

&lt;p&gt;怎么去取一个动态的阈值，才能保证后续处理的数据是整体的90%左右呢？&lt;/p&gt;

&lt;p&gt;考虑到batch normalization，通过减均值、除方差的形式来对每层的输出进行归一化，其中有moving average的技巧，
那是不是有类似的方案，可以通过滑动窗口计算分位数呢？ 经过一番搜索，终于看到了一个可能可行的方案：&lt;/p&gt;

\[\begin{array}{l}
m_0 = x_0 \\
if  \quad x_i &amp;lt; m_{i-1}  \quad m_i=m_{i-1}-\frac{\delta}{p} \\
else \quad if \quad x_i &amp;gt; m_{i-1}  \quad m_i=m_{i-1}+\frac{\delta}{1-p} \\
else \quad m_i=m_{i-1}
\end{array}\]

&lt;p&gt;在该方案中，$\delta$的取值比较关键，通过$\delta_i=\sigma_i\dot r$来获取，r是指定的超参，而$\sigma_i$是通过moving average计算出来的标准差。&lt;/p&gt;

&lt;p&gt;在&lt;a href=&quot;https://mjambon.com/2016-07-23-moving-percentile/&quot;&gt;该链接&lt;/a&gt;中，作者认为r的取值从0.001到0.01都可以，但是，经过实际测试，发现r的取值较大时，
分位数的均值接近预期，但是方差会很大。尤其是分位数较小或者较大(0.99， 0.999)时，r的取值需要更小才行。&lt;/p&gt;

&lt;p&gt;为了接近这个问题，尝试对分位数再进行moving average,结果会稳定很多。如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/moving_percentile.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据分布确定时，r决定了分位数的方差：r越小，方差越小；r越大，方差越大&lt;/li&gt;
  &lt;li&gt;相同的r下，分位数也会有一定影响，p(1-p)越小，准确性越差&lt;/li&gt;
  &lt;li&gt;建议添加一次moving average，来获取更稳定的分位数均值&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/moving_percentile_2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;示例代码参见：&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/moontree/moontree.github.io/blob/master/examples/moving_percentile.py&quot;&gt;代码&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://mjambon.com/2016-07-23-moving-percentile/&quot;&gt;Windowless percentile tracking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average&quot;&gt;Moving average&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2021/10/18/Moving-Percentile/</link>
        <guid isPermaLink="true">https://moontree.github.io/2021/10/18/Moving-Percentile/</guid>
        
        
        <category>math</category>
        
        <category>sliding-window</category>
        
      </item>
    
      <item>
        <title>Gumbel-Softmax</title>
        <description>&lt;p&gt;在读 &lt;a href=&quot;https://ojs.aaai.org//index.php/AAAI/article/view/5925&quot;&gt;Stochastic Loss Function&lt;/a&gt; 这篇论文的时候，
发现了一个之前从未听过的名词：Gumbel Softmax，在一探究竟的同时做下简单记录。&lt;/p&gt;

&lt;h2 id=&quot;gumbel-softmax-trick&quot;&gt;Gumbel-Softmax Trick&lt;/h2&gt;

&lt;h3 id=&quot;背景&quot;&gt;背景&lt;/h3&gt;
&lt;p&gt;在强化学习中，如果动作空间是离散的，比如上、下、左、右四个动作，通常的做法是网络输出一个四维的one-hot向量(不考虑空动作)，
分别代表四个动作。比如[1,0,0,0]代表上，[0,1,0,0]代表下等等。
而具体取哪个动作呢，就根据输出的每个维度的大小，选择值最大的作为输出动作,即argmax(𝑣)。&lt;/p&gt;

&lt;p&gt;例如网络输出的四维向量为𝑣=[−20,10,9.6,6.2]，第二个维度取到最大值10，那么输出的动作就是[0,1,0,0]，也就是说，这和多类别的分类任务是一个道理。
但是这种取法有个问题是不能计算梯度，也就不能更新网络。通常的做法是加softmax函数，把向量归一化，这样既能计算梯度，同时值的大小还能表示概率的含义。&lt;/p&gt;

&lt;p&gt;将$𝑣=[−20,10,9.6,6.2]$通过softmax函数后有$𝜎(𝑣)=[0,0.591,0.396,0.013]$，
这样做不会改变动作或者说类别的选取，同时softmax倾向于让最大值的概率显著大于其他值，
比如这里10和9.6经过softmax放缩之后变成了0.591和0.396，6.2对应的概率更是变成了0.013，
这有利于把网络训成一个one-hot输出的形式，这种方式在分类问题中是常用方法。&lt;/p&gt;

&lt;p&gt;但是这么做还有一个问题，这个表示概率的向量𝜎(𝑣)=[0,0.591,0.396,0.013]并没有真正显示出概率的含义，
因为一旦某个值最大，就选择相应的动作或者分类。比如𝜎(𝑣)=[0,0.591,0.396,0.013]和𝜎(𝑣)=[0,0.9,0.1,0]在类别选取的结果看来没有任何差别，
都是选择第二个类别，但是从概率意义上讲差别是巨大的。&lt;/p&gt;

&lt;p&gt;对于分类问题来说，softmax已经足够使用了，但是如果我们想用这个概率值去生成样本，用于后续训练，argmax()就不再适用了。
但是直接按照概率进行采样，会导致梯度无法回传，该怎么办呢？ Re-parameterization Trick就是用了解决这个问题的。&lt;/p&gt;

&lt;h3 id=&quot;re-parameterization-trick&quot;&gt;Re-parameterization Trick&lt;/h3&gt;
&lt;p&gt;从自编码器开始说起，原始的自编码器是这样子的：图像经过网络映射到embedding，decoder部分从embedding重建图像。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/self_encoder_ori.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而VAE并不是直接取提取特征向量，而是提取图像的分布特征:均值和标准差，再根据均值和标准差采样生产特征向量样本，用于重建图像。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/vae.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果将采样步骤写在计算图里的话，这部分就没法计算梯度了，而重参数技巧就是用来解决这个问题的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/reparameterise.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设图中的𝑥和𝜙表示VAE中的均值和标准差向量，它们是确定性的节点。而需要输出的样本𝑧是带有随机性的节点，
重参数就是把带有随机性的𝑧变成确定性的节点，同时随机性用另一个输入节点𝜖代替。
例如，这里用正态分布采样，原本从均值为𝑥和标准差为𝜙的正态分布$𝑁(𝑥,𝜙^2)$中采样得到𝑧。
将其转化成从标准正态分布𝑁(0,1)中采样得到𝜖,再计算得到$𝑧=𝑥+𝜖⋅𝜙$。
这样一来，采样的过程移出了计算图，整张计算图就可以计算梯度进行更新了，
而新加的𝜖的输入分支不做更新，只当成一个没有权重变化的输入。&lt;/p&gt;

&lt;p&gt;整个过程和batch normalization比较像，只不过是用随机输入代替了inference时的输入。&lt;/p&gt;

&lt;h3 id=&quot;gumbel-softmax-trick-1&quot;&gt;Gumbel-Softmax Trick&lt;/h3&gt;

&lt;p&gt;VAE的例子是一个连续分布（正态分布）的重参数，引入了一定的随机性，而离散分布的采样，就需要用到Gumbel-Softmax了。&lt;/p&gt;

&lt;p&gt;假设每个类别的概率是$p_1, p_2, …, p_k$，可以按照如下方式依照概率对类别进行采样,称为iGumbel-Max，后续给出两者等价的证明。&lt;/p&gt;

\[argmax (\log p_i - \log(-\log \epsilon_i))_{i=1}^k, \epsilon_i∼U[0,1]\]

&lt;p&gt;也就是说，先算出各个概率的对数$\log p_i$，然后从均匀分布$U[0,1]$中采样k个随机数$\epsilon_1,…,\epsilon_k$，
把$\log(−\log \epsilon_i)$加到$\log p_i$上去，最后把最大值对应的类别抽取出来就行了。
通过这种方式，随机性转移到U[0,1]上，并且不带有未知参数，完成了离散分布的重参数过程。&lt;/p&gt;

&lt;p&gt;为了保证可到，将GumbelMax替换为光滑版本-Gumbel Softmax&lt;/p&gt;

\[softmax((\log p_i − \log(−\log \epsilon_i))/\tau)^k_{i=1} ,\epsilon_i∼U[0,1]\]

&lt;p&gt;&lt;img src=&quot;/static/img/gumbel_sample_of_different_t.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;$\tau &amp;gt; 0$ 为退火参数，值越小，结果就越接近one-hot形式，越大就越接近等概率抽样。 有一个小技巧：
如果$p_i$是softmax的输出，那么大可不必先算出$p_i$再取对数，直接将$\log p_i$替换为$o_i$即可：&lt;/p&gt;

\[softmax((\log o_i − \log(−\log \epsilon_i))/\tau)^k_{i=1} ,\epsilon_i∼U[0,1]\]

&lt;p&gt;证明过程如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/gumbel_max.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;gumbel-distribution&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Gumbel_distribution&quot;&gt;Gumbel Distribution&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Gumbel Distribution 是一个关于”最大值”的概率的分布，比如已知过去100年河流的水位情况，那么gumbel分布可以用于预测明年河流的最大水位分布，
会给出每个值是”最大值”的概率。&lt;/p&gt;

\[z = \frac{x - \mu}{\beta}\]

&lt;p&gt;累积分布函数如下：&lt;/p&gt;

\[F(x:\mu,\beta)=e^{-e^z}\]

&lt;p&gt;概率密度函数如下：&lt;/p&gt;

\[f(x:\mu,\beta)=\frac{1}{\beta}e^{-(z + e^{-z})}\]

&lt;p&gt;&lt;img src=&quot;/static/img/gumbel_distribuction.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/moontree/moontree.github.io/blob/master/examples/gumbel_distribuction.py&quot;&gt;代码&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ojs.aaai.org//index.php/AAAI/article/view/5925&quot;&gt;Stochastic Loss Function&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Gumbel_distribution&quot;&gt;Gumbel Distribution&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1611.01144.pdf&quot;&gt;CATEGORICAL REPARAMETERIZATION WITH GUMBEL-SOFTMAX&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kexue.fm/archives/6705/comment-page-1&quot;&gt;漫谈重参数：从正态分布到Gumbel Softmax&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/initial-h/p/9468974.html&quot;&gt;Gumbel-Softmax Trick和Gumbel分布&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 15 Oct 2021 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2021/10/15/Gumbel-Softmax-Trick/</link>
        <guid isPermaLink="true">https://moontree.github.io/2021/10/15/Gumbel-Softmax-Trick/</guid>
        
        
        <category>deeplearning</category>
        
        <category>math</category>
        
      </item>
    
      <item>
        <title>风格迁移系列</title>
        <description>&lt;h1 id=&quot;风格迁移&quot;&gt;风格迁移&lt;/h1&gt;

&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;

&lt;p&gt;图像的风格迁移是一项很有意思的研究，比如把你的自拍变成动漫风格，或者把你拍的风景画转变为梵高的风格，瞬间就高大上了不少。&lt;/p&gt;

&lt;p&gt;而另一个问题，则是出于实际工作的需求：
在短视频推荐的时候，我们需要给用户推荐他喜欢的视频的相似视频，又不能完全一样。&lt;/p&gt;

&lt;p&gt;而视觉的相似性，在相似视频的判断里起着极大地作用。目前判断相似使用的特征有若干种来源：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;传统算法的特征&lt;/li&gt;
  &lt;li&gt;分类任务的特征&lt;/li&gt;
  &lt;li&gt;自监督任务的特征&lt;/li&gt;
  &lt;li&gt;行为数据训练的特征&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;2很好理解，最常见的就是使用ImageNet进行分类训练，然后取其中某层的输出作为特征，进行相似性计算。
但是这样会有一个很明显的问题，ImageNet的分类力度太粗了，很多同一类别的东西在视觉上看起来并不相似，
比如狗，有各种各样品种的狗、全身的、只有头部的等，很难对类内进行更细致的刻画。&lt;/p&gt;

&lt;p&gt;而Instance级别的自监督任务则可以部分解决这个问题，这种方法的通常做法是，把每张图片视为一个类别，
同一张图片的不同数据增强，视为正样本；不同图片的数据增强，视为负样本。这样训练得到的特征
可以增加图片之间的区分性，比单纯的分类任务得到的特征要好些。&lt;/p&gt;

&lt;p&gt;以din为代表的行为数据训练，则是从另一个角度出发，认为同一个用户的行为序列具有内在的联系，将用户点击过的
内容进行编码，通过avg pool或者attention得到用户特征。在这种方案下，内容特征是一个附属产物，可能比较有效，
但是可解释性上甚至不如前面两者。&lt;/p&gt;

&lt;p&gt;2、3的本质都是分类任务，只不过是类别的粒度不同，然而也都是在整张图片这个维度上进行的。
这样会导致一些问题，比如三段式这样的视频，他们的内容不同，但是计算得到的相似度很高，主要原因
就是图片的大部分区域是相同的。要解决这样的问题，可能有很多种方案，
比如去除无意义的区域、只对图片主体进行相似度计算；或者case by case，进行裁剪处理等。&lt;/p&gt;

&lt;p&gt;但是这样并为抓住本质问题–相似是有区别的，内容的相似、风格的相似。&lt;/p&gt;

&lt;p&gt;内容的相似很好理解，图片的内容是什么，有什么物体等；但是风格相似，就有些不好定义了，是图片的配色？还是纹理？或者是其他的什么东西？&lt;/p&gt;

&lt;p&gt;如果说，我们能够显式的给出风格特征和内容特征，是不是可以提供更多维度的相似呢？
比如对下图而言，从左到右依次为图像A，B，C：我们希望，A和C的风格特征是一致的，B和C的内容特征是一致的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/style_transfer_example.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但是，怎么去做到这点呢？让我们来读一下已有的论文吧。&lt;/p&gt;

&lt;h2 id=&quot;风格迁移的原理&quot;&gt;风格迁移的原理&lt;/h2&gt;
&lt;ol&gt;
  &lt;li&gt;两张图像经过预训练好的分类网络，若提取出的高维特征(high-leval)之间的欧氏距离越小，则这两张图像内容越相似；&lt;/li&gt;
  &lt;li&gt;两张图像经过预训练好的分类网络，若提取出的低维特征(low-level)在数值上基本相等，则这两张图像风格越相似，
换句话说，两张图像相似等价于二者特征Gram的矩阵具有较小的弗罗贝尼乌斯范数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;基于这两点，就可以设计合适的损失函数优化网络。&lt;/p&gt;

&lt;h3 id=&quot;image-style-transfer-using-convolutional-neural-networks&quot;&gt;&lt;a href=&quot;https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf&quot;&gt;Image Style Transfer Using Convolutional Neural Networks&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这篇论文应该是采用CNN来进行风格迁移的开山之作，基本思想很简单，基于优化方法来进行风格迁移：optimization-based&lt;/p&gt;

&lt;p&gt;有原图$\vec{p}$， 风格图片$\vec{a}$, 生成的图片$\vec{x}$,&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;内容相似度：
$P^l$ 和 $F^l$ 表示第l层的特征表示，使用MSE loss来表示他们的内容相似度：&lt;/li&gt;
&lt;/ul&gt;

\[L_{content}(\vec{p}, \vec{x}, l) = \frac{1}{2}\sum_{i,j}(F_{ij}^l - P_{ij}^l)^2\]

&lt;ul&gt;
  &lt;li&gt;风格相似度：
参考&lt;a href=&quot;https://arxiv.org/pdf/1505.07376.pdf&quot;&gt;Texture Synthesis Using Convolutional Neural Networks&lt;/a&gt; 这篇文章，
将纹理视为风格，并且使用特征之间的相关作为风格的表达，根据Gram矩阵进行计算。
$G_{ij}^l$表示第l层，feature map i 和 j 表示为向量之后的内积。&lt;/li&gt;
&lt;/ul&gt;

\[G_{ij}^l = \sum_k F_{ik}^lF_{jk}^l\]

&lt;p&gt;第l层提供的loss为&lt;/p&gt;

\[E_l = \frac{1}{4N_l^2M_l^2}\sum_{i,j}(G_{ij}^l - A_{ij}^l)^2\]

\[L_{style}(\vec{a},\vec{x}) = \sum_{l=0}^Lw_lE_l\]

&lt;ul&gt;
  &lt;li&gt;在风格和内容之间进行权衡，得到整体loss:&lt;/li&gt;
&lt;/ul&gt;

\[L_{total}(\vec{p},\vec{a},\vec{x}) = \alpha L_{content}(\vec{p},\vec{x}) + \beta L_{style} (\vec{a}, \vec{x})\]

&lt;p&gt;以下两张图片可以更好地帮助理解：&lt;/p&gt;

&lt;p&gt;内容重构和风格重构结果
&lt;img src=&quot;/static/img/content_and_style_reconstruction.png&quot; alt=&quot;内容重构和风格重构结果&quot; /&gt;&lt;/p&gt;

&lt;p&gt;整体训练流程&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/style_transfer_algorithm.png&quot; alt=&quot;整体训练流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然而，我们很容易发现，由于目标图片是一个优化loss的过程，每次得到结果都要运行很长时间，对大部分人来说是难以接受的。
也就有了一个自然而然的想法，能不能对每种风格，只训练一次呢？然后只输入内容图片，就可以得到目标图片呢？
或者更进一步，能不能任意输入风格图片和内容图片，经过很短时间就可以获取目标图片呢？&lt;/p&gt;

&lt;p&gt;方法总是有的，我们来看下下面的这篇论文。&lt;/p&gt;

&lt;h3 id=&quot;perceptual-losses-for-real-time-style-transfer-and-super-resolution&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1603.08155.pdf&quot;&gt;Perceptual Losses for Real-Time Style Transfer and Super-Resolution&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;与上一篇论文不同，这篇论文通过一个AutoEncoder来拟合风格迁移的过程，相当于把风格记在了网络的权重中，
这样在inference的时候，只需要输入内容图片就可以了。&lt;/p&gt;

&lt;p&gt;下图是流程的描述，很容易理解：
&lt;img src=&quot;/static/img/perceptual_loss_algorithm.png&quot; alt=&quot;训练流程&quot; /&gt;&lt;/p&gt;

&lt;p&gt;网络由两部分组成，第一部分是AutoEncoder的结构，对输入图片进行变换，得到输出图片；
第二部分是预训练好的VGG网络，用于计算第一步的图片和目标图片的loss;这部分网络的权重是固定的，采用的loss和上篇论文也是一致的。&lt;/p&gt;

&lt;p&gt;inference的时候，第一部分输出的结果就是生产的图片。&lt;/p&gt;

&lt;p&gt;通过loss的监督，将风格记录在了第一部分网络的权重中，从而减少了optimize的过程，有效加快了风格生成的速度。
然而，似乎哪里不太对……前者可以生产任意风格的图片，但是这篇论文的方法，每个模型只能生产一种风格的图片啊！似乎为了速度，损失了通用性。&lt;/p&gt;

&lt;p&gt;有没有一种方法，既可以保持迅速的inference过程，又可以适用任何风格呢？&lt;/p&gt;

&lt;h3 id=&quot;stylebank-an-explicit-representation-for-neural-image-style-transfer&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1703.09210.pdf&quot;&gt;StyleBank: An Explicit Representation for Neural Image Style Transfer&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;很暴力的一种方案，既然论文2已经可以将一种风格保存在模型里了，那么我在模型里同时保留多个风格，然后选择用哪个风格来进行迁移，不就完成了吗？
所以这就有StyleBank这篇论文，暴力，但是可用。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/stylebank.png&quot; alt=&quot;stylebank&quot; /&gt;&lt;/p&gt;

&lt;p&gt;看图也很好理解，模型由三部分组成，Encoder E, StyleBank Layer K, Decoder D, E+D用于内容的重建，而K负责控制不同风格。&lt;/p&gt;

&lt;p&gt;这样的设计，有两个好处：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;增加新的style时可以采用incremental training。锁住E,D，初始化一个新的K进行训练就好，速度很快。&lt;/li&gt;
  &lt;li&gt;可以比较轻松的进行Region-specific style fusion，即将照片不同的区域转化成不同的style。对E输出的feature map F加不同的mask，再与对应的StyleBank layer卷积就好&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;细节： 训练的时候，训练T次（E+K+D)和1次（E+D)。&lt;/p&gt;

&lt;p&gt;stylebank在一定程度上丰富了风格的选择，但是还是做不到任意风格的迁移。&lt;/p&gt;

&lt;h3 id=&quot;a-learned-representation-for-artistic-style&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1610.07629.pdf&quot;&gt;A Learned Representation for Artistic Style&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这篇Google的则在之前instance normalization的基础上，提出了conditional instance normalization。
主要思想是，很多不同的艺术风格在有很多相同的视觉元素，这部分没有必要在每个风格里都去计算、保存。
在实验中，作者发现，instance normalizaion中的scale和shift足以表达不同的风格。&lt;/p&gt;

\[z = \gamma_s(\frac{x - \mu } {\sigma}) + \beta_s\]

&lt;p&gt;IN的公式和BN基本一致，每次normalized后会有一次scale and shift的操作。
而所谓conditional IN，则是不同的style采用不同的scale and shift参数, 也就是图中的$\gamma$和$\beta$ 。
训练过程中每个style分别优化自己的$\gamma$和$\beta$，将feature转换到自己的空间。
而其他层如conv layer，则所有style共用相同的参数。
本质上，即是将每个style投射成一个embedding。（将所有的$\gamma$和$\beta$拼起来，大约有3000维）&lt;/p&gt;

&lt;p&gt;这部分想法，相当于使用一个AutoEncoder来对风格进行训练、重塑，只不过直接利用了IN中的scale and shift。&lt;/p&gt;

&lt;p&gt;可以发现风格迁移的进化过程很有意思：
1、基于优化的方式，从白噪声出发，直接从内容图片、风格图片优化得到目标图片
2、将内容重构、单个风格作为模型的任务，进行训练优化，单次inference得到目标图片
3、将内容重构、风格模型进行分离，内容部分是一个Encoder-Decoder结构，风格部分保存在另一部分权重里
4、将风格部分也做成一个Encoder-Decoder结构，通过参数控制不同风格；然后进入内容部分的Decoder里&lt;/p&gt;

&lt;p&gt;其实，到4这部分，就已经可以满足我们的业务需求了——风格、内容都有了自己对应的embedding，可以用来做些好玩的事情了。如果有很多很多的风格、内容图片进行训练，就可以验证想法对不对了。&lt;/p&gt;

&lt;p&gt;但是，单独从风格迁移这个任务来讲的话，又有了一个新的坑——前面的工作最多只能处理32种风格，没有办法处理没见过的风格。怎样才能
处理任意的风格图片呢？&lt;/p&gt;

&lt;p&gt;坑都挖好了，肯定是要有人来填的，果不其然，有些工作就开始填坑了：&lt;/p&gt;

&lt;h3 id=&quot;exploring-the-structure-of-a-real-time-arbitrary-neural-artistic-stylization-network&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1705.06830.pdf&quot;&gt;Exploring the Structure of a Real-time, Arbitrary Neural Artistic Stylization Network&lt;/a&gt;)&lt;/h3&gt;

&lt;p&gt;这篇论文是上一篇的延展，既然每种风格能够用3000维的embedding表示，是不是给定一个网络，能够在inference过程中得到这3000多维向量，就可以了呢？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/style_predict.png&quot; alt=&quot;stylebank&quot; /&gt;&lt;/p&gt;

&lt;p&gt;说干就干，他们做了这样一个改变：增加了一个风格预测网络P，预测任意一张风格图片的embedding，也就是上篇论文中的$\gamma$和$\beta$。和上篇论文的主要区别是
embedding的获取方式，上篇论文相当于是一个统计值，类似center_loss中的center，在每次训练中更新并保存下来；而这片论文则是用了一个网络去进行风格的拟合。&lt;/p&gt;

&lt;p&gt;这个其实是上节4中提到的，将风格做成了一个完整的Encoder-Decoder结构，但是单独作为一个一篇论文，似乎有些取巧……&lt;/p&gt;

&lt;h3 id=&quot;arbitrary-style-transfer-in-real-time-with-adaptive-instance-normalization--github&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1703.06868.pdf&quot;&gt;Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization&lt;/a&gt;  &lt;a href=&quot;https://github.com/xunhuang1995/AdaIN-style&quot;&gt;github&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;作者认为不同的style其实由feature的variance和mean决定，因此通过将Content image的feature 转换，
使其与style image的feature有相同的variance和mean即可实现风格迁移。&lt;/p&gt;

&lt;p&gt;这种方法没有进行风格学习的参数，只要进行如下转换即可：&lt;/p&gt;

\[AdaIn(x, y) = \sigma(y)(\frac{(x - \mu (x)}{\sigma(x)}) + \mu (y)\]

&lt;p&gt;&lt;img src=&quot;/static/img/adain.png&quot; alt=&quot;stylebank&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图两个Encoder均为pre-trained VGG19，
训练过程中参数不再更新。Training和Inference过程中，Content和Style image同时传入Encoder，
得到两组feature map， 然后通过AdaIN，对content image进行变换，
转换所用的参数由计算得到，然后再传给decoder生成最终图片。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;需要注意的是 Content loss和之前paper定义的有所不同，如图所示，用的是经过AdaIN变换过的feature map计算L2 loss，而非encoder的输出&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;小结&quot;&gt;小结&lt;/h3&gt;
&lt;p&gt;关于风格和内容的区分：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;高层特征的表达的是内容&lt;/li&gt;
  &lt;li&gt;低维特征的Gram的矩阵表示风格&lt;/li&gt;
  &lt;li&gt;采用IN之后，mean 和 variance 表示风格&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;风格迁移网络的进化：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;基于优化的方式，从白噪声出发，直接从内容图片、风格图片优化得到目标图片&lt;/li&gt;
  &lt;li&gt;将内容重构、单个风格作为模型的任务，进行训练优化，单次inference得到目标图片&lt;/li&gt;
  &lt;li&gt;将内容重构、风格模型进行分离，内容部分是一个Encoder-Decoder结构，风格部分保存在另一部分权重里&lt;/li&gt;
  &lt;li&gt;将风格部分也做成一个Encoder-Decoder结构，通过参数控制不同风格；然后进入内容部分的Decoder里&lt;/li&gt;
  &lt;li&gt;直接对特征进行变化，将内容特征的mean和variance转变为风格图片的值，进行重构即可&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;代码及效果尝试todo&quot;&gt;代码及效果尝试（TODO）&lt;/h2&gt;

&lt;h2 id=&quot;参考文档&quot;&gt;参考文档&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/57564626&quot;&gt;Style Transfer 风格迁移综述&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1505.07376.pdf&quot;&gt;Texture Synthesis Using Convolutional Neural Networks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1607.08022.pdf&quot;&gt;Instance Normalization：The Missing Ingredient for Fast Stylization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf&quot;&gt;Image Style Transfer Using Convolutional Neural Networks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1603.08155.pdf&quot;&gt;Perceptual Losses for Real-Time Style Transfer and Super-Resolution&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1703.09210.pdf&quot;&gt;StyleBank: An Explicit Representation for Neural Image Style Transfer&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1610.07629.pdf&quot;&gt;A Learned Representation for Artistic Style&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1705.06830.pdf&quot;&gt;Exploring the Structure of a Real-time, Arbitrary Neural Artistic Stylization Network&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1703.06868.pdf&quot;&gt;Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization&lt;/a&gt;  &lt;a href=&quot;https://github.com/xunhuang1995/AdaIN-style&quot;&gt;github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 05 Nov 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/11/05/%E9%A3%8E%E6%A0%BC%E8%BF%81%E7%A7%BB%E7%B3%BB%E5%88%97/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/11/05/%E9%A3%8E%E6%A0%BC%E8%BF%81%E7%A7%BB%E7%B3%BB%E5%88%97/</guid>
        
        
        <category>cv</category>
        
        <category>style-transfer</category>
        
      </item>
    
      <item>
        <title>C++中的中文正则</title>
        <description>&lt;h2 id=&quot;疑惑现象&quot;&gt;疑惑现象&lt;/h2&gt;
&lt;p&gt;对于给定的一个字符串，只保留中文、英文、数字，其他特殊符号和标点、空格都要去掉。该怎么做呢？&lt;/p&gt;

&lt;p&gt;很直观的一个想法就是用正则来解决，比如如下代码：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;std::string normalize(const std::string&amp;amp; input) {
    std::regex pattern1(&quot;[^a-zA-Z0-9\u2e80-\u2fd5\u3190-\u319f\u3400-\u4dbf\u4e00-\u9fcc\uf900-\ufaad]&quot;);
    std::string charset_range_fmt = regex_replace(input,
                                                  pattern1,
                                                  &quot;&quot;,
                                                  std::regex_constants::match_default);

    return charset_range_fmt;
}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然而，事实证明，我们想的太简单了，跑了几个例子，输入和输出如下所示：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;表哥❗️~10月10破记录刘二狗【 ==normalize== 表哥❗️10月10破记录刘二狗【
美女,,，。。否 ==normalize== 美女，。。否
 -\`ajfaf/-_~`     ==normalize== ajfaf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;看起来有些字符确实被去掉了，但有些没有，这究竟是为什么呢？是因为没去掉的字符在给出的范围内吗？并不是，
通过工具，可以看到❗对应的编码是’\u2757’，理应被我们的正则去掉的。那是什么原因呢？&lt;/p&gt;

&lt;h2 id=&quot;解决方案&quot;&gt;解决方案&lt;/h2&gt;
&lt;p&gt;想一下正则的原理，本质上还是使用动态规划去按字符比较，而C++中，std::string的每个元素都是char，
而unicode编码需要使用多个字节来进行表示，会导致字节粒度的匹配有些问题。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/unicode-bytes.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果单独的字符和前后能够满足正则匹配的话，也不会被去掉。具体分析如下:&lt;/p&gt;

&lt;p&gt;而为了解决这个问题，需要用到wstring，在wstring里面，基本元素是wchar_t，用于表示中文。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;std::wstring wnormalize(const std::wstring input) {
    std::wregex pattern1(L&quot;[^a-zA-Z0-9\u2e80-\u2fd5\u3190-\u319f\u3400-\u4dbf\u4e00-\u9fcc\uf900-\ufaad]&quot;);
    std::wstring target = L&quot;&quot;;
    std::wstring charset_range_fmt = regex_replace(input,
                                                   pattern1,
                                                   target,
                                                  std::regex_constants::match_default);

    return charset_range_fmt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;为了使用wnormalize，还需要将string转换为wstring，可以用如下代码完成两者的相互转换：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#include &amp;lt;locale&amp;gt;
#include &amp;lt;codecvt&amp;gt;
std::wstring_convert&amp;lt;std::codecvt_utf8_utf16&amp;lt;wchar_t&amp;gt;&amp;gt; converter;
std::wstring witem = converter.from_bytes(items[i]);
converter.to_bytes(wnormalize(witem))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://www.cnblogs.com/zizifn/p/4716712.html&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 22 Oct 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/10/22/C++%E4%B8%AD%E7%9A%84%E4%B8%AD%E6%96%87%E6%AD%A3%E5%88%99/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/10/22/C++%E4%B8%AD%E7%9A%84%E4%B8%AD%E6%96%87%E6%AD%A3%E5%88%99/</guid>
        
        
        <category>cpp</category>
        
        <category>wstring</category>
        
        <category>regex</category>
        
      </item>
    
      <item>
        <title>自监督学习</title>
        <description>&lt;p&gt;&lt;img src=&quot;/static/img/self-supervised-compare.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;综述&quot;&gt;综述&lt;/h2&gt;
&lt;p&gt;在监督学习中，系统会给定一个输入x和一个标签y，来使模型输出正确的标签，而最后一层的embedding也可以用在其他任务上。
而自监督学习中，标签y并未给定，那就需要想办法来找到一个标签。&lt;/p&gt;

&lt;p&gt;按照找到的标签来说，自监督学习可以分为两类：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;生成式：这类方法主要关注像素的重建误差，大多数以像素的loss为主。包括AutoEncoder, VAE， GAN等。
对编码器的基本和要求是尽可能保留原始数据的重要信息，如果能够通过decoder解码回原始图片，说明
latent code重建的足够好了。
&lt;img src=&quot;/static/img/autoencoder.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

    &lt;p&gt;但是，这种方法有一个问题：&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;进行像素重建的计算开销非常大&lt;/li&gt;
      &lt;li&gt;GAN的方式会是任务变得复杂而且难以优化&lt;/li&gt;
    &lt;/ul&gt;

    &lt;p&gt;在&lt;a href=&quot;https://ankeshanand.com/blog/2020/01/26/contrative-self-supervised-learning.html&quot;&gt;这篇博客&lt;/a&gt;中，有一个很好地例子。
对于一张钱币，我们能够很容易地分辨真假，说明在识别真假这个任务里，我们的特征已经学习的很好了；但是，画一张同样的纸币出来，几乎是不可能的。
这个例子告诉我们，重建是好特征表达的充分条件，但不是必要条件。就有了判别式的方法。
&lt;img src=&quot;/static/img/dollar-bill-drawing.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;/static/img/dollar-bill-detailed.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;判别式：这类方法并不要求模型能够重建原始输入，而是希望模型能够在特征空间上对不同的输入进行分辨，就像上面美元的例子。&lt;/p&gt;

    &lt;p&gt;这类方法有以下特点：&lt;/p&gt;
    &lt;ol&gt;
      &lt;li&gt;在 feature space 上构建距离度量；&lt;/li&gt;
      &lt;li&gt;通过特征不变性，可以得到多种预测结果；&lt;/li&gt;
      &lt;li&gt;使用 Siamese Network；&lt;/li&gt;
      &lt;li&gt;不需要 pixel-level 重建。&lt;/li&gt;
    &lt;/ol&gt;

    &lt;p&gt;而这类方法的主要任务，就是如何构造正样本和负样本。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;两类方法的结构如下所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/generative_vs_contrastive.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;contrastive-based&quot;&gt;Contrastive Based&lt;/h2&gt;

&lt;p&gt;从前面我们知道，由一个原始的 input 去建模一个 high-level representation 是很难的，这也是自监督学习想做的事情。
其中常用的策略是：future，missing 和 contextual，即预测未来的信息，
比如 video 中当前帧预测后面的帧；丢失的信息或者是上下文的信息，
比如 NLP 里面的 word2vec 和 BERT。&lt;/p&gt;

&lt;h3 id=&quot;contrastive-predictive-coding&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1807.03748.pdf&quot;&gt;Contrastive Predictive Coding&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;这篇文章主要是通过 contrastive 的方式在 speech, images, text 和
 在reinforcement learning 中都取得了很好的效果。&lt;/p&gt;

&lt;p&gt;对于语音和文本，可以充分利用了不同的 k 时间步长，来采集正样本，而负样本可以从序列随机取样来得到。
&lt;img src=&quot;/static/img/cpc-base.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;对于图像任务，可以使用 pixelCNN 的方式将其转化成一个序列类型，用前几个 patch 作为输入，预测下一个 patch。
&lt;img src=&quot;/static/img/cpc-image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;deep-infomax&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1808.06670.pdf&quot;&gt;Deep InfoMax&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;和CPC不同，Deep InfoMax并没有对图像进行patch，而是在feature维度上进行的操作：
对global_feature和local_feature进行了判别，
正样本是图片的global feature和中间某个feature map的m*m个local feature，负样本是其他图片的local feature。
&lt;img src=&quot;/static/img/deep-infomax.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;contrastive-multiview-coding&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1906.05849.pdf&quot;&gt;Contrastive MultiView Coding&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;CMC这篇论文充分利用了各个通道的信息，比如同一张图片的 RGB图 和 深度图。
而且通过这个方式，每个 anchor 不仅仅只有一个正样本，可以通过多模态得到多个正样本。
&lt;img src=&quot;/static/img/cmc.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;memory-bank&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1805.01978.pdf&quot;&gt;Memory Bank&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;我们可以发现，在上述过程中，负样本的数量有很多，但是一个batch获取到的负样本总是有限的，
怎么利用大量的负样本呢？把计算过的特征存起来，下次直接使用不就可以了吗？
Memory Bank应运而生，把之前模型产生样本特征全部存起来，
当前计算损失的时候直接拿来用就可以了，每次模型更新完后将当前的特征重新更新到 memory bank 中，
以便下一次使用。这个工作的缺点就在于每次需要将所有样本的特征全部存起来。
后续 kaiming 大神提出的 Moco[28]，
主要的贡献是 Momentum Update、 shuffleBN 等技术点来优化这个过程。
&lt;img src=&quot;/static/img/memroy-bank.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;moco&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/1911.05722.pdf&quot;&gt;MOCO&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;首先，从NLP的无监督例如BERT出发 ，作者指出Computer Vision的无监督学习需要建立dictionary：
因为数据信号是连续的，分布在空间高维，并且不像NLP那样结构化。
将现有的无监督方法归类为dictionary learning之后，
作者提出建立dictionary依赖两个必要条件：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;large，dictionary的大小需要足够大，才能对高维、连续空间进行很好的表达；&lt;/li&gt;
  &lt;li&gt;consistent，dictionary的key需要使用相同或者相似的encoder进行编码，
这样query和key之间的距离度量才能够一致并且有意义。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/moco-base.png&quot; alt=&quot;&quot; /&gt;
如图，large这个条件是通过一个先进先出队列实现，
consistent这个条件通过momentum更新的encoder来实现，
类似于DL里面常用的exponential decay技巧。&lt;/p&gt;

&lt;p&gt;下图给出了伪代码&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/moco-code.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;simclr&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/2002.05709.pdf&quot;&gt;SimCLR&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/static/img/simclr-architecture.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;随机采样一个batch；
对batch里每张图像做两种增强，可以认为是两个view；
让同一张图的不同view在latent space里靠近，不同图的view在latent space里远离，
通过NT-Xent实现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/simclr-code.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这样的话，可以通过增加batch size来增加负样本数量，更利于多机扩展。&lt;/p&gt;

&lt;h3 id=&quot;byol&quot;&gt;&lt;a href=&quot;https://arxiv.org/pdf/2006.07733.pdf&quot;&gt;BYOL&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;上述方案都需要负样本，BYOL则使用了另一种方式避开了负样本的存在。
直接去拉近正样本对之间的特征距离。&lt;/p&gt;

&lt;p&gt;论文基于这样一个发现：
两个随机初始化的网络A和B，输入同一个样本的不同augmentation版本，A的输出特征作为B的监督信息，
A的参数固定不动，B训练，训练到最后B的特征表达能力竟然青出于蓝，比A还要好，
那如果再弄第三个网络C去跟B学，第四个网络D跟C学，
就可以像梯云纵那样，左脚踩右脚，螺旋升天，法力无边。
相比之下，后面用moving average / mean teacher的具体实现，
只不过是把上述交替学习的过程变得小步快跑，更为高效而已，并不是最核心的贡献了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/byol.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;contrastive learning框架有两大目标：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;不同的原样本有不同的表征，负样本的存在就是为了确保这个目标&lt;/li&gt;
  &lt;li&gt;同一个原样本的不同augmentation结果 / view有相同的表征&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;之前其实已经有挺多工作告诉我们，一个随机初始化的CNN就已经具备一定程度的图像特征抽取能力。
更进一步地，如果CNN随机初始化之后，就已经会把不同的样本输入投射到特征空间的不同地方，
并且投射得足够分散，那它就已经完成了contrastive learning框架中第一个目标。
如果上述猜测成立，那么只要在接下来的训练过程中达成contrastive learning框架第二个目标，
并且小心翼翼地维护第一个目标不被破坏，避免网络收敛到trivial solution，
那就确实可以抛开负样本。
而这个小心翼翼维护的操作，在BYOL里面就体现为”teacher不要太快跟上student的步伐“，
论文Table 5说明，跟得太快会破坏contrastive特性，跟得太慢又有损训练效率，
为了trade-off，就祭出了mean teacher这个好用的工具，平衡稳定性与效率。&lt;/p&gt;

&lt;h2 id=&quot;参考文献&quot;&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/108625273&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://towardsdatascience.com/generating-images-with-autoencoders-77fd3a8dd368&quot;&gt;Comprehensive Introduction to Autoencoders&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ankeshanand.com/blog/2020/01/26/contrative-self-supervised-learning.html&quot;&gt;Contrastive Self-Supervised Learning&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;https://www.zhihu.com/question/402452508/answer/1293771636&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/2003.14323.pdf&quot;&gt;How Useful is Self-Supervised Pretraining for Visual Tasks&lt;/a&gt;定义了 Utility 来衡量 SSL 方法的效率，测试了众多 SSL 方法在各种任务下不同的有效性 。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/2003.05438.pdf&quot;&gt;Rethinking Image Mixture for Unsupervised Visual Representation Learning&lt;/a&gt; 对比学习里的 Mixup&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/2006.10029.pdf&quot;&gt;Big Self-Supervised Models are Strong Semi-Supervised Learners&lt;/a&gt;SimCLR作者的新作，大力出奇迹。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;&quot;&gt;Bootstrap Your Own Latent A New Approach to Self-Supervised Learning&lt;/a&gt;不需要制造负样本，model 之间互相迭代 teaching 就可以得到很好的 performance。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/2005.10243.pdf&quot;&gt;What Makes for Good Views for Contrastive Learning&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;https://www.zhihu.com/question/402452508/answer/1294166177&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/pdf/1807.03748.pdf&quot;&gt;Contrastive Predictive Coding&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 09 Sep 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/09/09/self-supervised-learning/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/09/09/self-supervised-learning/</guid>
        
        
        <category>cv</category>
        
        <category>self-supervised-learning</category>
        
      </item>
    
      <item>
        <title>导向滤波</title>
        <description>&lt;p&gt;在&lt;a href=&quot;https://moontree.github.io/2020/10/19/%E4%BB%8Ebilateral-filter-%E5%88%B0-HDRnet/&quot;&gt;bilateral filter 到 HDRnet&lt;/a&gt;这篇文章里，
我们提到了双边滤波可以较好的保持图像的边缘，但是它也有着自己的问题：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;如果一个像素周围的相似像素较少，会出现梯度反转现象&lt;/li&gt;
  &lt;li&gt;暴力的双边滤波需要$O(Nr^2)$的时间&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而导向滤波（guided filter),则可以在保持梯度的同时也有着极大的速度优势，计算时间是线性的O(N)&lt;/p&gt;

&lt;p&gt;导向滤波（Guided Filtering）和双边滤波（BF）、最小二乘滤波（WLS）是三大边缘保持（Edge-perserving）滤波器。
当然，引导滤波的功能不仅仅是边缘保持，只有当引导图是原图的时候，它就成了一个边缘保持滤波器。&lt;/p&gt;

&lt;h2 id=&quot;原理&quot;&gt;原理&lt;/h2&gt;
&lt;p&gt;导向滤波的原理图如下所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/guided_filter.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;对于输入图像p，通过引导图像I，经过滤波后得到输入图像q，导向滤波是这样计算的：&lt;/p&gt;

\[q_i = a_kI_i + b_k, \forall  i \in w_k \\
q_i = p_i - n_i\]

&lt;p&gt;导向滤波的一个重要假设是输出图像q和引导图像I在滤波窗口$w_k$上存在局部线性关系。
这样的话，在一个局部区域里，如果引导图像I有一个边缘，输出图像q也保持边缘不变，
因为对于相邻的像素点而言，存在$\bigtriangledown q=\alpha \bigtriangledown I$。
因此，只要求到了系数a,b，也就得到了输出。&lt;/p&gt;

&lt;p&gt;同时，我们认为，输入图像p是由输出图像q加上我们不希望的噪声或纹理n得到，因此有p=q+n。&lt;/p&gt;

&lt;p&gt;接下来，就是解出这样的系数，使得p、q的差别尽可能小。对于每一个滤波窗口，该算法在最小二乘意义
上的最优化，可以表示为：&lt;/p&gt;

\[argmin \sum_{i \in w_k}(q_i - p_i)^2 \\
argmin \sum_{i \in w_k}(a_kI_i + b_k - p_i)^2\]

&lt;p&gt;最后，引入一个正则化参数$\epsilon$,防止$a_k$过大，于是有：&lt;/p&gt;

\[E(a_k,b_k) = \sum_{i \in w_k}((a_kI_i + b_k - p_i)^2 + \epsilon a_k^2)\]

&lt;p&gt;对上述方程进行求解，有:&lt;/p&gt;

\[\frac{\delta E}{a_k} = \sum_{i \in w_k} (2(a_kI_i + b_k - p_i)I_i + 2 \epsilon a_k) = 0 \\
\frac{\delta E}{b_k} = \sum_{i \in w_k}(2(a_kI_i + b_k - p_i)) = 0 \\
a_k = \frac{\sum_{i \in w_k}p_iI_i - b_k \sum_{i \in w_k}I_i}{\sum_{i \in w_k}(I_i^2 + \epsilon)} \\
b_k = \frac{1}{|w|}(\sum_{i \in w_k}p_i - a_k \sum_{i \in w_k}I_i)\]

&lt;p&gt;将$b_k$带入$a_k$，整理可得：&lt;/p&gt;

\[a_k=\frac{\frac{1}{|w|}\sum_{i \in w_k}I_ip_i - \mu_k \overline{p}_k}{\sigma_k^2 + \epsilon} \\
b_k = \overline{p}_k - a_k \mu_k\]

&lt;p&gt;在这里，$\mu_k$和$\sigma_k^2$分别表示引导图像I在窗口$w_k$中的平均值和方差，
|w|表示窗口$w_k$内像素点的个数，
\(\overline{p}_k = \frac{1}{|w|}\sum_{i \in w_k}p_i\) 表示输入图像在窗口中的平均值。&lt;/p&gt;

&lt;p&gt;接下来，将上述线性模型应用到整个图像的滤波窗口即可。但是，每个像素点会被包含到多个窗口里：
如果用3*3的窗口滤波，除了边缘位置，每个点都会抱包含在9个窗口里；每个窗口都会有一个$q_i$值，
对所有的值进行平均，得到最终结果：&lt;/p&gt;

\[q_i=\frac{1}{|w|}\sum_{k:i \in w_k}(a_kI_i + b_k)
=\overline{a}_iI_i + \overline{b}_i\]

&lt;p&gt;这样，就建立里每个像素点从I到q的映射。&lt;/p&gt;

&lt;h2 id=&quot;边缘保持&quot;&gt;边缘保持&lt;/h2&gt;
&lt;p&gt;对于该算法，输入和引导图像是同一副图像也就是I=p的时候,该算法成为一个边缘保持滤波器，方程的接表示为：&lt;/p&gt;

\[a_k = \frac{\sigma_k^2}{\sigma_k^2 + \epsilon} \\
b_k = (1 - a_k)\overline{p}_k\]

&lt;p&gt;在这种情况下，$\epsilon$相当于界定平滑区域和便与其按区域的阈值。考虑如下两种情况：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Case 1：平坦区域。如果在某个滤波窗口内，该区域是相对平滑的，方差$\sigma_k^2$将远远小于$\epsilon$。
从而$a_k \approx 0,b_k \approx \overline{p}_k$。相当于对该区域作均值滤波。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/guided_filter_smooth.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Case 2：高方差区域。相反，如果该区域是边缘区域，方差很大,
方差$\sigma_k^2$将远远大于$\epsilon$。
从而$a_k \approx 1,b_k \approx 0$。相当于在区域保持原有梯度。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/guided_filter_edge_preserve.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;其他应用&quot;&gt;其他应用&lt;/h2&gt;
&lt;p&gt;羽化、去雾等，请见&lt;a href=&quot;http://kaiminghe.com/eccv10/eccv10ppt.pdf&quot;&gt;ppt&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;代码&quot;&gt;代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/moontree/moontree.github.io/blob/master/examples/guided_filter.py&quot;&gt;链接&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://blog.csdn.net/weixin_43194305/article/details/88959183&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/161666126&lt;/li&gt;
  &lt;li&gt;http://kaiminghe.com/eccv10/&lt;/li&gt;
  &lt;li&gt;http://kaiminghe.com/eccv10/eccv10ppt.pdf&lt;/li&gt;
  &lt;li&gt;http://kaiminghe.com/publications/eccv10guidedfilter.pdf&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/36813673&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 09 Sep 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/09/09/guided-filter/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/09/09/guided-filter/</guid>
        
        
        <category>cv</category>
        
        <category>edge-perserving-filter</category>
        
      </item>
    
      <item>
        <title>Tensorflow Embedding Lookup</title>
        <description>&lt;p&gt;  对于从事深度学习的人来说，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf.nn.embedding_lookup&lt;/code&gt;这个函数应该并不罕见，活跃在各种embedding领域-文本、用户等。&lt;/p&gt;

&lt;p&gt;  然而今天，一个小伙伴在把checkpoint导出为pb的时候，遇到了如下错误：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ValueError: Cannot create a tensor proto whose content is larger than 2GB.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;  错误表达的很明确，转换成pb格式的时候，单个proto 不能大于2G，可是怎么去解决呢？&lt;/p&gt;

&lt;h2 id=&quot;embedding_lookup的分区&quot;&gt;embedding_lookup的分区&lt;/h2&gt;

&lt;p&gt;拿这个错误去搜索了一下，发现embedding_lookup可以从多个变量里去获取查询，这样的话，
可以把一个很大的查询表拆分成若干个小的表，就可以转换成pb了。&lt;/p&gt;

&lt;p&gt;  下面的代码给出了使用的例子和输出结果：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;per_shard_nums = 10
# 10 * 1

weights = []

for i in range(100):
    emb = tf.get_variable(
        'words-%02d' % i,
        initializer=tf.constant(
            # np.reshape(np.arange(i * per_shard_nums, i * per_shard_nums + per_shard_nums), (-1, 1))
            np.arange(i * per_shard_nums, i * per_shard_nums + per_shard_nums)
        )
    )
    weights.append(emb)

idx = tf.placeholder(tf.int64, shape=[None])

embedding = tf.nn.embedding_lookup(weights, idx, partition_strategy='mod')
embedding_2 = tf.nn.embedding_lookup(weights, idx, partition_strategy='div')

sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
# print(sess.run(weights))

print(sess.run(embedding, feed_dict={idx: [0, 1, 2, 99, 999, 100]}))
# res: [  0  10  20 990 999   1]
print(sess.run(embedding_2, feed_dict={idx: [0, 1, 2, 99, 999, 100]}))
# res: [  0   1   2  99 999 100]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;  我们构造了这样一个例子：有1000个id，每个id对应的值是自身；将这1000个值放到100个variable里面，使用embedding_lookup进行查询。&lt;/p&gt;

&lt;p&gt;  细心的同学可能已经发现了，有一个参数叫做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;partition_strategy&lt;/code&gt;，这个是用来干什么的呢，有什么区别呢？&lt;/p&gt;

&lt;p&gt;  按照如上方式构造查询表之后，查询表的大小可以视为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[num_shards, per_shard_num, embedding_len]&lt;/code&gt;，而&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;partition_strategy&lt;/code&gt;决定的，就是确定分区和每个分区内index的方式。
假设要查询的编号为index,
那么’mod’对应的分区和行号为：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;line, shard = divmod(index, num_shard)&lt;/code&gt;,
而’div’模式对应的分区和行号为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shard, line = divmod(index, per_shard_num)&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;   回到代码中的例子，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_shards=100, per_shard_num=10&lt;/code&gt;, 对于99这个序号，
‘mod`模式计算如下：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;line, shard = divmod(99, 100) = (99, 0), val = 990
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;   &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;div&lt;/code&gt;模式计算如下：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;line, shard = divmod(99, 10) = (9， 9), val = 99
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;其他的类似可得。&lt;/p&gt;

&lt;h2 id=&quot;partitionedvariable与partitioner&quot;&gt;PartitionedVariable与partitioner&lt;/h2&gt;
&lt;p&gt;   查看函数的注释，可以发现如下内容：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;partitioner.

  If `len(params) &amp;gt; 1`, each element `id` of `ids` is partitioned between
  the elements of `params` according to the `partition_strategy`.
  In all strategies, if the id space does not evenly divide the number of
  partitions, each of the first `(max_id + 1) % len(params)` partitions will
  be assigned one more id.

  If `partition_strategy` is `&quot;mod&quot;`, we assign each id to partition
  `p = id % len(params)`. For instance,
  13 ids are split across 5 partitions as:
  `[[0, 5, 10], [1, 6, 11], [2, 7, 12], [3, 8], [4, 9]]`

  If `partition_strategy` is `&quot;div&quot;`, we assign ids to partitions in a
  contiguous manner. In this case, 13 ids are split across 5 partitions as:
  `[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10], [11, 12]]`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;  注释帮助我们更形象地理解了partition的作用，但是第一段话也给我带来了疑问：
id space实际上是人为指定的，为什么要指定一个不能整除的最大值呢？在权重初始化的时候也不方便啊，
除非有某个函数，可以指定总数和分区数？那就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf.get_variable&lt;/code&gt;这个函数了，看一下它的参数，果然有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;partitioner&lt;/code&gt;这个参数，
一路追踪进去，看到了如下注释：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;If a partitioner is provided, a `PartitionedVariable` is returned.
Accessing this object as a `Tensor` returns the shards concatenated along
the partition axis.

Some useful partitioners are available.  See, e.g.,
`variable_axis_size_partitioner` and `min_max_variable_partitioner`.

partitioner: Optional callable that accepts a fully defined `TensorShape`
    and `dtype` of the Variable to be created, and returns a list of
    partitions for each axis (currently only one axis can be partitioned).

The `partitioner` must be a callable that accepts a fully defined
  `TensorShape` and returns a sequence of integers (the `partitions`).
  These integers describe how to partition the given sharded `Variable`
  along the given dimension.  That is, `partitions[1] = 3` means split
  the `Variable` into 3 shards along dimension 1.  Currently, sharding along
  only one axis is supported.

  If the list of variables with the given name (prefix) is already stored,
  we return the stored variables. Otherwise, we create a new one.

  If initializer is `None` (the default), the default initializer passed in
  the constructor is used. If that one is `None` too, we use a new
  `glorot_uniform_initializer`. If initializer is a Tensor, we use
  it as a value and derive the shape from the initializer.

  If the initializer is a callable, then it will be called for each
  shard.  Otherwise the initializer should match the shape of the entire
  sharded Variable, and it will be sliced accordingly for each shard.

  Some useful partitioners are available.  See, e.g.,
  `variable_axis_size_partitioner` and `min_max_variable_partitioner`.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;   还是云里雾里的，不如直接用代码来说话吧：&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;partitioner = tf.fixed_size_partitioner(100, axis=0)
with tf.variable_scope(&quot;embedding&quot;, partitioner=partitioner):
    weights = tf.get_variable(
        &quot;weights&quot;,
        [1000],
        initializer=np.arange(1000),
        partitioner=partitioner
    )

idx = tf.placeholder(tf.int64, shape=[None])

embedding = tf.nn.embedding_lookup(weights, idx, partition_strategy='mod')
embedding_2 = tf.nn.embedding_lookup(weights, idx, partition_strategy='div')

sess = tf.InteractiveSession()
sess.run(tf.global_variables_initializer())
# print(sess.run(weights))

print(sess.run(embedding, feed_dict={idx: [0, 1, 2, 99, 999, 100]}))
# res: [  0.  10.  20. 990. 999.   1.]
print(sess.run(embedding_2, feed_dict={idx: [0, 1, 2, 99, 999, 100]}))
# res: [  0.   1.   2.  99. 999. 100.]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;  可以发现，这段代码实现了和第一段代码一样的功能，使用的都是tf自带的函数。
其中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf.fixed_size_partitioner&lt;/code&gt;可以将参数在指定维度（axis）分割成指定份数（num_shards),
要想使用该接口，需要搭配&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tf.variable_scope&lt;/code&gt;才行，
值得注意的是，这样得到的weights是一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PartitionedVariable&lt;/code&gt;类型的变量，不能直接通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sess.run()&lt;/code&gt;获取到。&lt;/p&gt;

&lt;p&gt;  那么，对参数进行切分有什么用处呢？可以解决开始提到的单个variable过大问题。如果只是为了解决这个问题，是不是有些大材小用了呢？&lt;/p&gt;

&lt;p&gt;  其实，这个设计是为了更高效的进行分布式训练，考虑这样一个场景：
在tensorflow的ps架构中，ps负责存储模型的参数，worker负责使用训练数据对参数进行更新。
默认情况下，tensorflow会把参数按照round-robin的方式放到各个参数服务器（ps）上。
例如，模型有5个参数（注意这五个参数都是tensor而非标量），P1，P2, P3, P4, P5，由2个ps（记为ps0和ps1）负责存放，
则P1,P3,P5会存放在ps0上，P2,P4会存放在ps1上。
显然，如果P1,P3,P5都比P2,P4大，那ps0上存放的参数会远大于ps1。
这样在更新参数时，ps0的网络就有可能成为瓶颈。&lt;/p&gt;

&lt;p&gt;  造成参数分布不均匀的主要原因在于tensorflow在为各个参数分配ps时，只是在参数这个粒度做的，粒度太大。
如，参数A的大小为1024&lt;em&gt;1024，参数B的大小为2&lt;/em&gt;2。
在这种情况下，如果不对A和B进行分割，无论如何分配都无法做到均匀。
如果把A和B分割成512&lt;em&gt;1024 + 512&lt;/em&gt;1024和1&lt;em&gt;2 + 1&lt;/em&gt;2，那在两台ps上就可以均匀分配。&lt;/p&gt;

&lt;p&gt;  tensorboard效果如下所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/variable_patition&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;  训练时长对比：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://img-blog.csdn.net/20180515235120100&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;  ps流量对比：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://img-blog.csdn.net/20180515235210685&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;本文代码地址&quot;&gt;本文代码地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/moontree/moontree.github.io/blob/master/examples/tf_embedding_lookup.py&quot;&gt;链接&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;参考&quot;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://www.cnblogs.com/gongxijun/p/9995960.html&lt;/li&gt;
  &lt;li&gt;https://www.jianshu.com/p/abea0d9d2436&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/u013431916/article/details/80330813&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 07 Sep 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/09/07/tf-embedding-lookup/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/09/07/tf-embedding-lookup/</guid>
        
        
        <category>tensorflow</category>
        
        <category>embedding_lookup</category>
        
      </item>
    
      <item>
        <title>Tone Mapping（色调映射）</title>
        <description>&lt;p&gt;读论文的时候，看到了”Tone Mapping”这个超出我知识范围的词语，就很好奇，这究竟是个什么东西呢？
就把了解到的信息做个记录吧，方便以后查阅。&lt;/p&gt;

&lt;h2 id=&quot;hdr与ldr&quot;&gt;HDR与LDR&lt;/h2&gt;

&lt;h3 id=&quot;hdrhigh-dynamic-range&quot;&gt;HDR(High Dynamic Range)&lt;/h3&gt;
&lt;p&gt;什么是HDR，首先我们来理解DR——Dynamic Range（动态范围）：Dynamic Range是一种用数学方式来描述某个给定场景的亮度层次范围的技术术语。
指图像中所包含的从“最亮”至“最暗”的比值，也就是图像从“最亮”到“最暗”之间灰度划分的等级数；
动态范围越大，所能表示的层次越丰富，所包含的色彩空间也越广。&lt;/p&gt;

&lt;p&gt;最通常的解释有两种：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;一种是摄影界通常所说的D值（以对数值表示的场景最高亮度和最低亮度比的相对数值），通常由0-4之间的很精确的数字来表示。
D值的计算公式为：Dynamic Range=log10(Max Intensity / Min Intensity)。
公式中intensity是指光照强度，我们对最大亮度除以最低亮度的结果取对数，得到的结果就是动态范围的相对数值——摄影界所说的D值。
各种景物、底片和照片都有其各自特定的D值范围。&lt;/li&gt;
  &lt;li&gt;另一种是计算机图形学中通常使用的直接以场景最高亮度和最低亮度的亮度比表述的方法，如255:1。
在数字图像领域一般都采用这第二种比值的表述方式来评述场景的动态范围。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;亮度的单位以每平方米的烛光来表示（cd/m2）。
太阳自身的亮度大约为1,000,000,000 cd/m2。
阳光照射下的景物的亮度可达100,000 cd/m2，而星光的亮度大约在0.001 cd/m2以下，二者亮度比达亿倍以上。
现实中人类的眼睛所能看到亮度比的范围是$10^5$左右，相对于255:1来说，我们称之为高动态范围，即HDR。&lt;/p&gt;

&lt;h3 id=&quot;ldrlow-dynamic-range-image&quot;&gt;LDR(Low Dynamic Range Image)&lt;/h3&gt;
&lt;p&gt;什么是LDR？它所采用的色彩模型是目前通用的图像描述模型——RGB模型。
每种色彩都可以用三原色（红、绿、蓝）加上适当的亮度来表示，三原色的亮度梯度各为256级。
选定每色256级是在电脑硬件性能、照片级真彩图片需要和电脑2进制方案综合考虑后的结果。
这就是目前我们非常熟习的观看、编辑、交换和处理数字图像的软硬件环境。
这种8比特位元RGB低动态范围图像描述模型是将场景最高亮度和最低亮度的亮度比限定为255比1，计算得出的动态范围D值即为2.4。&lt;/p&gt;

&lt;h2 id=&quot;tone-mapping&quot;&gt;Tone Mapping&lt;/h2&gt;
&lt;p&gt;Tone mapping自古以来一直都有，不是计算机图形学的专利。
早期因为颜料的对比度有限，达芬奇等的高手会把需要表达的内容用很有限的颜色画出来，即便色彩不真实。
而刚发明电影的时候，胶片能表达的亮度范围有限，
所以摄影师会把高亮区域和阴影区域向中等亮度方向压缩，发展出了S曲线的映射关系。
这些都是tone mapping。&lt;/p&gt;

&lt;p&gt;下面，我们来看下相关算法的发展,&lt;/p&gt;

&lt;h3 id=&quot;经验派-reinhard-tone-mapping&quot;&gt;经验派-&lt;a href=&quot;http://www.cmap.polytechnique.fr/~peyre/cours/x2005signal/hdr_photographic.pdf&quot;&gt;Reinhard tone mapping&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;Reinhard tone mapping非常简单，用代码描述就三行。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;float3 ReinhardToneMapping(float3 color, float adapted_lum) {
    const float MIDDLE_GREY = 1;
    color *= MIDDLE_GREY / adapted_lum;
    return color / (1.0f + color);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中color是线性的HDR颜色，adapted_lum是根据整个画面统计出来的亮度。
MIDDLE_GREY表示把什么值定义成灰。这个值就是纯粹的magic number了，根据需要调整。
Reinhard的曲线是这样的，可以看出总体形状是个S型。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/reinhard_curve.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这种tone mapping的方法更多地来自于经验，没什么原理在后面。
所以就姑且称它为经验派吧。它的优点是简单直接，把亮的变暗，暗的变量。
这样暗处和亮处细节就都出来了。但缺点也很明显，就是灰暗。
个个颜色都朝着灰色的方向被压缩了，画面像蒙了一层纱。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/reinhard_result.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;粗暴派&quot;&gt;粗暴派&lt;/h3&gt;
&lt;p&gt;到了2007年，孤岛危机（Crysis）的CryEngine 2，为了克服Reinhard灰暗的缺点，
开始用了另一个tone mapping的方法。
前面提到了tone mapping就是个S曲线，那么既然你要S曲线，
我就搞出一个S曲线。这个方法更简单，只要一行，而且没有magic number。
用一个exp来模拟S曲线。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;float3 CEToneMapping(float3 color, float adapted_lum)
{
    return 1 - exp(-adapted_lum * color);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;CE的曲线中间的区域更偏向于小的方向，这部分曲线也更陡。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/ce_curve.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个方法得到的结果比Reinhard有更大的对比度，颜色更鲜艳一些，虽然还是有点灰。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/ce_result.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;CE的方法在于快速，并且视觉效果比Reinhard。但是这个方法纯粹就是凑一个函数，没人知道应该如何改进。属于粗暴地合成。&lt;/p&gt;

&lt;h3 id=&quot;拟合派-filmic-tone-mapping&quot;&gt;拟合派-Filmic tone mapping]&lt;/h3&gt;

&lt;p&gt;这个方法的本质是把原图和让艺术家用专业照相软件模拟胶片的感觉，
人肉tone mapping后的结果去做曲线拟合，得到一个高次曲线的表达式。
这样的表达式应用到渲染结果后，就能在很大程度上自动接近人工调整的结果。&lt;/p&gt;

&lt;p&gt;最后出来的曲线是这样的。总的来说也是S型，但增长的区域很长。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/filmic_curve.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;从结果看，对比度更大，而且完全消除了灰蒙的感觉。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/filmic_result.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而代码就有点复杂了：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;float3 F(float3 x)
{
	const float A = 0.22f;
	const float B = 0.30f;
	const float C = 0.10f;
	const float D = 0.20f;
	const float E = 0.01f;
	const float F = 0.30f;

	return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
}

float3 Uncharted2ToneMapping(float3 color, float adapted_lum)
{
	const float WHITE = 11.2f;
	return F(1.6f * adapted_lum * color) / F(WHITE);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;那些ABCDEF都是多项式的系数，而WHITE是个magic number，表示白色的位置。
这个方法开启了tone mapping的新路径，让人们知道了曲线拟合的好处。
并且，其他颜色空间的变换，比如gamma矫正，也可以一起合并到这个曲线里来，一次搞定，不会增加额外开销。
缺点就是运算量有点大，两个多项式的计算，并且相除。&lt;/p&gt;

&lt;p&gt;因为Filmic tone mapping的优异表现，大部分游戏都切换到了这个方法。
包括CE自己，也在某个时候完成了切换。&lt;/p&gt;

&lt;h3 id=&quot;你们都是渣渣派-academy-color-encoding-systemaces&quot;&gt;你们都是渣渣派-&lt;a href=&quot;https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/&quot;&gt;Academy Color Encoding System（ACES&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;在大家以为Filmic tone mapping会统治很长时间的时候，江湖中来了一位异域高手。
他认为，你们这帮搞游戏/实时图形的，都是渣渣。让我们电影业来教你们什么叫tone mapping。
这位高手叫美国电影艺术与科学学会，就是颁布奥斯卡奖的那个机构。
不要以为他们只是个评奖的单位，美国电影艺术与科学学会的第一宗旨就是提高电影艺术与科学的质量。&lt;/p&gt;

&lt;p&gt;他们发明的东西叫Academy Color Encoding System（ACES），是一套颜色编码系统，或者说是一个新的颜色空间。
它是一个通用的数据交换格式，一方面可以不同的输入设备转成ACES，另一方面可以把ACES在不同的显示设备上正确显示。
不管你是LDR，还是HDR，都可以在ACES里表达出来。这就直接解决了VDR的问题，不同设备间都可以互通数据。&lt;/p&gt;

&lt;p&gt;然而对于实时渲染来说，没必要用全套ACES。
因为第一，没有什么“输入设备”。渲染出来的HDR图像就是个线性的数据，所以直接就在ACES空间中。
而输出的时候需要一次tone mapping，转到LDR或另一个HDR。
也就是说，我们只要ACES里的非常小的一条路径，而不是纷繁复杂的整套体系。&lt;/p&gt;

&lt;p&gt;那么这条路径有多小呢？只要几行，系数来自于Krzysztof Narkowicz的博客文章。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;float3 ACESToneMapping(float3 color, float adapted_lum)
{
	const float A = 2.51f;
	const float B = 0.03f;
	const float C = 2.43f;
	const float D = 0.59f;
	const float E = 0.14f;

	color *= adapted_lum;
	return (color * (A * color + B)) / (color * (C * color + D) + E);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;很像Uncharted 2的做法，都是多项式拟合。但是式子比Uncharted的简单，并不需要算两个多项式并相除，只要算一个，一次搞定。它的曲线是这样的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/aces_curve.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;S感很浓，并且比Uncharted的更向小的方向移，即便很小的数值和接近1的数值也有梯度。这样能很好地保留暗处和亮处的细节。至于视觉效果如何呢？看看这个。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/aces_result.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;可以看出来，比之前的任何一个都要鲜艳，并且没有因此丢掉细节！当之无愧成为目前最好的tone mapping算法。
更好的地方是，按照前面说的，ACES为的是解决所有设备之间的颜色空间转换问题。
所以这个tone mapper不但可以用于HDR到LDR的转换，还可以用于从一个HDR转到另一个HDR。
也就是从根本上解决了VDR的问题。这个函数的输出是线性空间的，所以要接到LDR的设备，只要做一次sRGB校正。
要接到HDR10的设备，只要做一次Rec 2020颜色矩阵乘法。Tone mapping部分是通用的，这也是比之前几个算法都好的地方。&lt;/p&gt;

&lt;p&gt;目前一些新的游戏，比如Rise of the Tomb Raider、UE 4.8，也都切换到ACES的tone mapping曲线。&lt;/p&gt;

&lt;h2 id=&quot;gamma-correction&quot;&gt;Gamma Correction&lt;/h2&gt;
&lt;p&gt;早期的 CRT 显示器存在非线性输出的问题,简单来说,你给 CRT 显示器输入(input)一个 0.5(输入范围为[0,1]),
CRT 显示器的输出(output)并不是 0.5,而是约等于 0.218,输入与输出间存在一个指数大概为 2.2 的幂次关系:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-8d410eeea31b29f5b522e7629e471a20_b.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;其中 2.2 这个指数即为伽马(gamma)值,而显示器的这种非线性输出过程则称为伽马展开(display gamma).
为了能够得到正确的输出,我们必须对输入进行补偿,方法就是对输入进行一次指数为 $1 / 2.2$ 的幂次运算,
这个补偿的过程便是伽马编码(encoding gamma)我们也称gamma correction :&lt;/p&gt;

&lt;p&gt;$input -&amp;gt; input^{\frac{1}{2.2})$&lt;/p&gt;

&lt;p&gt;所以为了让显示器正确输出 0.5, 我们需要对 0.5 进行伽马校正,实际给显示器的输入约为 0.73。&lt;/p&gt;

&lt;p&gt;看到这里你可能会有个疑问：
既然伽马校正起源于早期 CRT 显示器的非线性输出问题,
而我们现在基本已经淘汰掉这些显示器,并且当今的显示器已经可以做到线性输出了(输入0.5,输出也是0.5),
那么我们是不是可以直接废弃伽马校正了呢?答案可能有些出人意料 :
我们仍然需要进行伽马校正!原因有些巧合 : 伽马校正除了可以解决早期 CRT 显示器的非线性输出问题,
同时还可以帮助我们”改善”输出的图像质量，下图是自然界中的亮度以及对应的人所感受的亮度值 :&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://picb.zhimg.com/v2-5da4673b8149b499df7202f54317e2d3_b.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;由上图可以看出，人眼对于较暗(接近0)的亮度值比较敏感,对于较亮(接近1)的亮度值则不太敏感，
你可以理解为人眼更能辨别较暗的亮度值发生的变化，因此颜色在存储时，我们应该更多的保存较暗部分的颜色值。
对于自然界中大约0.218的亮度值，人感受亮度值约为0.5，因此我们有了下面的Gamma Correction和CRT gamma曲线。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/crt_gamma.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设我们现在使用一个字节(能够表达整数范围[0,255])来存储亮度值,并且我们要存储 0.240 和 0.243 这两个亮度值,如果不进行伽马校正,则有:&lt;/p&gt;

&lt;p&gt;value1=0.240∗255=61.2⟶取整为61
value2=0.243∗255=61.965⟶取整为61&lt;/p&gt;

&lt;p&gt;可以看到 0.240 和 0.243 的存储数值都是 61,所以这两个输入的实际显示效果其实是一样的(细节差异丢失了).
但如果我们进行一次伽马校正,则有:
0.240 和 0.243 的存储数值变为了 133 和 134,
所以这两个输入的实际显示效果便区分开了(细节差异保留了).&lt;/p&gt;

&lt;p&gt;实际上,伽马校正增大了较暗数值的表示精度,而减小了较亮数值的表示精度,
人眼又恰好对较暗数值比较敏感,对较亮数值不太敏感,于是从视觉角度讲,输出的图像质量就被伽马校正”改善”了.
基于这个原因,我们仍然需要进行伽马校正,而既然我们进行了伽马校正,
当今的显示器也便保留了非线性输出(伽马展开)的功能。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;
&lt;p&gt;总的来说，Tone Mapping和Gamma Correction二者都是为了更好的在LDR设备上显示图片，
将图片的颜色值从一个范围分布变换到另一个范围分布。
而不同的是，Tone Mapping是根据相应的算法将颜色值从一个大的范围映射到了较小的范围，
而Gamma Correction则是从[0,1]映射到[0,1],
映射范围并没有改变，只是改变了不同亮度值颜色的分布情况。&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/Vince-Wu/p/12689602.html&quot;&gt;Gamma校正算法原理及实现&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/79203830&quot;&gt;Tone Mapping 与 Gamma Correction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/21983679&quot;&gt;Tone mapping进化论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 30 Aug 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/08/30/tone-mapping/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/08/30/tone-mapping/</guid>
        
        
        <category>cv</category>
        
        <category>style-transfer</category>
        
        <category>tone-mapping</category>
        
      </item>
    
      <item>
        <title>海量数据的相似检索</title>
        <description>&lt;p&gt;在海量数据中进行相似检索，是一个很常见的需求，主要用于各种召回阶段：
比如搜索场景的召回、相似视频检索等；相似物品的推荐等。
然而，现在是一个海量数据的行为，如何在短时间内完成向量距离的计算和检索，成为了一个亟待解决的问题。&lt;/p&gt;

&lt;p&gt;我们来看一下召回面临的两个问题：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;特征维度极大，比如bert特征是768维；user-item的click矩阵可以达到百万、千万维&lt;/li&gt;
  &lt;li&gt;item数量巨大，进行检索十分困难&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;针对这两个问题，有着不同的优化方案，
第一个问题就是特征降维；
第二个问题是优化检索方案-减少需要检索的数量。&lt;/p&gt;

&lt;h2 id=&quot;特征降维&quot;&gt;特征降维&lt;/h2&gt;
&lt;h3 id=&quot;pcaprincipal-component-analysis&quot;&gt;PCA(Principal Component Analysis)&lt;/h3&gt;
&lt;p&gt;PCA（Principal Component Analysis） 是一种常见的数据分析方式，常用于高维数据的降维，可用于提取数据的主要特征分量。&lt;/p&gt;

&lt;h3 id=&quot;minhashing&quot;&gt;Minhashing&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/min_hashing_a&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;以上图为例， 物品集合为{a,b,c,d,e}，
用户行为为 S1 = {a, b}, S2 = {c}, S3 = {b, d, e}, S4 = {a, c, d}。
如果在文本数据中，Si可以视为每个文本的bow向量，{a,b,c,d,e}可以视为词典集合。&lt;/p&gt;

&lt;p&gt;Minhashing是怎么做的呢？&lt;/p&gt;

&lt;p&gt;把原来元素{a, b, c, d, e} 顺序随机重排，比如下图，
排序结果为{b, e, a, d, c},
定义一个函数h：&lt;strong&gt;计算集合S最小的minhash值，就是在这种顺序下最先出现1的元素&lt;/strong&gt;。
那么h(S1) = a, h(S2)=c, h(S3)=b, h(S4)=a;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/min_hashing_b&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果进行n重排的话，就会有n个minhash函数，{h1(S), h2(S)…, hn(S)},
那原来每个高维集合，就会被降到n维空间，比如S1-&amp;gt;{h1(S1), h2(S1)…, hn(S1)}。&lt;/p&gt;

&lt;p&gt;但是在实际操作中，n的数量也是非常巨大的，整体重排需要的时间也很多，
通常会用若干随机哈希函数代替重排过程。
比如$h(x) = (i + 1) % 5 $，i表示对应的索引，从1开始，那么会有如下结果：&lt;/p&gt;

\[h_{S_1}(a) = 2 \\
h_{S_1}(d) = 0 \\
h_{S_2}(c) = 4 \\
h_{S_3}(b) = 3 \\
h_{S_3}(d) = 0 \\
h_{S_3}(e) = 1 \\
h_{S_4}(a) = 1 \\
h_{S_4}(c) = 4 \\
h_{S_4}(d) = 0 \\
minhash(h_{S_1}) = d \\
minhash(h_{S_2}) = c \\
minhash(h_{S_3}) = d \\
minhash(h_{S_4}) = d\]

&lt;h2 id=&quot;检索优化&quot;&gt;检索优化&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;基于树的方法&lt;/li&gt;
  &lt;li&gt;KD树是其下的经典算法。一般而言，在空间维度比较低时，KD树的查找性能还是比较高效的；但当空间维度较高时，该方法会退化为暴力枚举，性能较差，这时一般会采用下面的哈希方法或者矢量量化方法。&lt;/li&gt;
  &lt;li&gt;哈希方法&lt;/li&gt;
  &lt;li&gt;LSH(Locality-Sensitive Hashing)是其下的代表算法。文献[7]是一篇非常好的LSH入门资料。&lt;/li&gt;
  &lt;li&gt;对于小数据集和中规模的数据集(几个million-几十个million)，基于LSH的方法的效果和性能都还不错。这方面有2个开源工具FALCONN和NMSLIB。&lt;/li&gt;
  &lt;li&gt;矢量量化方法&lt;/li&gt;
  &lt;li&gt;矢量量化方法，即vector quantization。在矢量量化编码中，关键是码本的建立和码字搜索算法。比如常见的聚类算法，就是一种矢量量化方法。而在相似搜索中，向量量化方法又以PQ方法最为典型。&lt;/li&gt;
  &lt;li&gt;对于大规模数据集(几百个million以上)，基于矢量量化的方法是一个明智的选择，可以用用Faiss开源工具。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;kdtree&quot;&gt;KDTree&lt;/h3&gt;
&lt;p&gt;kd 树是一种对k维特征空间中的实例点进行存储以便对其快速检索的树形数据结构。
kd树是二叉树，核心思想是对 k 维特征空间不断切分:
假设特征维度是768，对于(0,1,2,…,767)中的每一个维度，以中值递归切分。
这样构造的树，每一个节点是一个超矩形，
小于结点的样本划分到左子树，大于结点的样本划分到右子树。
树构造完毕后，最终检索过程如下：
-（1）从根结点出发，递归地向下访问kd树。
若目标点当前维的坐标小于切分点的坐标，移动到左子树，否则移动到右子树，直至到达叶结点；
-（2）以此叶结点为“最近点”，递归地向上回退，
查找该结点的兄弟结点中是否存在更近的点，若存在则更新“最近点”，否则回退；
未到达根结点时继续执行（2）
-（3）回退到根结点时，搜索结束。&lt;/p&gt;

&lt;p&gt;对KD树选择从哪一维度进行开始划分的标准，采用的是求每一个维度的方差，然后选择方差最大的那个维度开始划分。
这里有一个比较有意思的问题是：为何要选择方差作为维度划分选取的标准？
我们都知道，方差的大小可以反映数据的波动性。
方差大表示数据波动性越大，选择方差最大的开始划分空间，可以使得所需的划分面数目最小，
反映到树数据结构上，可以使得我们构建的KD树的树深度尽可能的小。&lt;/p&gt;

&lt;p&gt;kd树在维数小于20时效率最高，一般适用于训练实例数远大于空间维数时的k近邻搜索；
当空间维数接近训练实例数时，它的效率会迅速下降，几乎接近线形扫描。&lt;/p&gt;

&lt;h3 id=&quot;balltree&quot;&gt;BallTree&lt;/h3&gt;

&lt;p&gt;为了解决kd树在样本特征维度很高时效率低下的问题，
研究人员提出了“球树“BallTree。
KD 树沿坐标轴分割数据，BallTree将在一系列嵌套的超球面上分割数据，
即使用超球面而不是超矩形划分区域。
具体而言，BallTree 将数据递归地划分到由质心 C 和 半径 r 定义的节点上，
以使得节点内的每个点都位于由质心C和半径 r 定义的超球面内。
通过使用三角不等式|X+Y|&amp;lt;=|x| + |Y|减少近邻搜索的候选点数。&lt;/p&gt;

&lt;h3 id=&quot;annoyapproximate-nearest-neighbors-oh-yeah&quot;&gt;Annoy(Approximate Nearest Neighbors Oh Yeah)&lt;/h3&gt;
&lt;p&gt;Annoy 同样通过建立一个二叉树来使得每个点查找时间复杂度是O(log n)，
和kd树不同的是，annoy没有对k维特征进行切分。
每次都是*随机选择两个点，以这两个节点为初始中心节点，执行聚类数为2的kmeans过程，最终产生收敛后两个聚类中心点。**
这两个聚类中心点之间连一条线段（灰色短线），
建立一条垂直于这条灰线，并且通过灰线中心
点的线（黑色粗线）。这条黑色粗线把数据空间分成两部分。
在多维空间的话，这条黑色粗线可以看成等距垂直超平面。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/annoy_split.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在划分的子空间内进行不停的递归迭代继续划分，直到每个子空间最多只剩下K个数据节点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/annoy_iter.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;最终原始数据会形成类似下面这样一个二叉树结构。
二叉树底层是叶子节点记录原始数据节点，其他中间节点记录的是分割超平面的信息。
Annoy建立这样的二叉树结构是希望满足这样的一个假设: 相似的数据节点应该在二叉树上位置更接近，
一个分割超平面不应该把相似的数据节点分割二叉树的不同分支上。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/annoy_tree.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;查询过程&quot;&gt;查询过程&lt;/h4&gt;
&lt;p&gt;如何进行对一个数据点进行查找相似节点集合呢？
查找的过程就是不断看他在分割超平面的哪一边。
从二叉树索引结构来看，就是从根节点不停的往叶子节点遍历的过程。
通过对二叉树每个中间节点（分割超平面相关信息）和查询数据节点进行相关计算
来确定二叉树遍历过程是往这个中间节点左孩子节点走还是右孩子节点走。
通过以上方式完成查询过程。&lt;/p&gt;

&lt;p&gt;然而实际情况并不会这么简单，&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;如果查询过程最终落到叶子节点的数据节点数小于 我们需要的Top N相似邻居节点数目怎么办？&lt;/li&gt;
  &lt;li&gt;两个相近的数据节点划分到二叉树不同分支上怎么办？
方案如下：
 (1) 如果分割超平面的两边都很相似，那可以两边都遍历
 (2) 建立多棵二叉树树，构成一个森林，每个树建立机制都如上面所述那样
 (3) 采用优先队列机制：采用一个优先队列来遍历二叉树，从根节点往下的路径，根据查询节点与当前分割超平面距离（margin）进行排序。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每棵树都返回一堆近邻点后，如何得到最终的Top N相似集合呢？
首先所有树返回近邻点都插入到优先队列中，求并集去重,
然后计算和查询点距离， 最终根据距离值从近距离到远距离排序，
返回Top N近邻节点集合。&lt;/p&gt;

&lt;h3 id=&quot;lshlocality-sensitive-hashing&quot;&gt;LSH(Locality-Sensitive Hashing)&lt;/h3&gt;

&lt;p&gt;LSH为我们提供了一种在海量的高维数据集中查找与查询数据点（query data point）
近似最相邻的某个或某些数据点。需要注意的是，
LSH并不能保证一定能够查找到与query data point最相邻的数据，
而是减少需要匹配的数据点个数的同时保证查找到最近邻的数据点的概率很大。&lt;/p&gt;

&lt;p&gt;基本思想是这样的：
将原始数据空间中的两个相邻数据点通过相同的映射或投影变换（projection）后，
这两个数据点在新的数据空间中仍然相邻的概率很大，
而不相邻的数据点被映射到同一个桶的概率很小。
也就是说，如果我们对原始数据进行一些hash映射后，
我们希望原先相邻的两个数据能够被hash到相同的桶内，
具有相同的桶号。
对原始数据集合中所有的数据都进行hash映射后，
我们就得到了一个hash table，
这些原始数据集被分散到了hash table的桶内，
每个桶会落入一些原始数据，属于同一个桶内的数据就有很大可能是相邻的，
当然也存在不相邻的数据被hash到了同一个桶内。
因此，如果我们能够找到这样一些hash functions，
使得经过它们的哈希映射变换后，
原始空间中相邻的数据落入相同的桶内的话，
那么我们在该数据集合中进行近邻查找就变得容易了，
我们只需要将查询数据进行哈希映射得到其桶号，
然后取出该桶号对应桶内的所有数据，
再进行线性匹配即可查找到与查询数据相邻的数据。
换句话说，我们通过hash function映射变换操作，
将原始数据集合分成了多个子集合，
而每个子集合中的数据间是相邻的且该子集合中的元素个数较小，
因此将一个在超大集合内查找相邻元素的问题转化为了在一个很小的集合内查找相邻元素的问题，
显然计算量下降了很多。&lt;/p&gt;

&lt;p&gt;那具有怎样特点的hash functions才能够使得原本相邻的两个数据点经过hash变换后会落入相同的桶内？
这些hash function需要满足以下两个条件：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;如果d(x,y) ≤ d1， 则h(x) = h(y)的概率至少为p1；&lt;/li&gt;
  &lt;li&gt;如果d(x,y) ≥ d2， 则h(x) = h(y)的概率至多为p2；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其中d(x,y)表示x和y之间的距离，d1 &amp;lt; d2， h(x)和h(y)分别表示对x和y进行hash变换。
满足以上两个条件的hash functions称为(d1,d2,p1,p2)-sensitive。
而通过一个或多个(d1,d2,p1,p2)-sensitive的hash function对原始数据集合进行hashing
生成一个或多个hash table的过程称为Locality-sensitive Hashing。&lt;/p&gt;

&lt;h4 id=&quot;查找过程&quot;&gt;查找过程&lt;/h4&gt;
&lt;p&gt;使用LSH进行对海量数据建立索引（Hash table）并通过索引来进行近似最近邻查找的过程如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;离线建立索引&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;选取满足(d1,d2,p1,p2)-sensitive的LSH hash functions；&lt;/li&gt;
  &lt;li&gt;根据对查找结果的准确率（即相邻的数据被查找到的概率）确定hash table的个数L，
每个table内的hash functions的个数K，以及跟LSH hash function自身有关的参数；&lt;/li&gt;
  &lt;li&gt;将所有数据经过LSH hash function哈希到相应的桶内，构成了一个或多个hash table；&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
  &lt;li&gt;在线查找&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;将查询数据经过LSH hash function哈希得到相应的桶号；&lt;/li&gt;
  &lt;li&gt;将桶号中对应的数据取出；（为了保证查找速度，通常只需要取出前2L个数据即可）；&lt;/li&gt;
  &lt;li&gt;计算查询数据与这2L个数据之间的相似度或距离，返回最近邻的数据；&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;增强lshamplifying-lsh&quot;&gt;增强LSH（Amplifying LSH）&lt;/h4&gt;
&lt;p&gt;通过LSH hash functions我们能够得到一个或多个hash table，
每个桶内的数据之间是近邻的可能性很大。
我们希望原本相邻的数据经过LSH hash后，
都能够落入到相同的桶内，
而不相邻的数据经过LSH hash后，
都能够落入到不同的桶中。
如果相邻的数据被投影到了不同的桶内，我们称为false negtive；
如果不相邻的数据被投影到了相同的桶内，我们称为false positive。
因此，我们在使用LSH中，我们希望能够尽量降低false negtive rate和false positive rate。&lt;/p&gt;

&lt;p&gt;为了达到这个目标，通常有两种方案：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;在一个hash table内使用更多的LSH hash function&lt;/li&gt;
  &lt;li&gt;建立多个hash table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以及一些常用方法：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;多个独立的hash table: 每次选用k个LSH hash function（同属于一个LSH function family）就得到了一个hash table，重复多次，即可创建多个hash table。多个hash table的好处在于能够降低false positive rate。&lt;/li&gt;
  &lt;li&gt;AND 操作： 从同一个LSH function family中挑选出k个LSH function，H(X) = H(Y)有且仅当这k个Hi(X) = Hi(Y)都满足。也就是说只有当两个数据的这k个hash值都对应相同时，才会被投影到相同的桶内，只要有一个不满足就不会被投影到同一个桶内。
AND与操作能够使得找到近邻数据的p1概率保持高概率的同时降低p2概率，即降低了falsenegtiverate。&lt;/li&gt;
  &lt;li&gt;OR 操作： 从同一个LSH function family中挑选出k个LSH function，H(X) = H(Y)有且仅当存在一个以上的Hi(X) = Hi(Y)。也就是说只要两个数据的这k个hash值中有一对以上相同时，就会被投影到相同的桶内，只有当这k个hash值都不相同时才不被投影到同一个桶内。
OR或操作能够使得找到近邻数据的p1概率变的更大（越接近1）的同时保持p2概率较小，即降低了false positive rate。&lt;/li&gt;
  &lt;li&gt;AND和OR的级联
将与操作和或操作级联在一起，产生更多的hahs table，这样的好处在于能够使得p1更接近1，而p2更接近0。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了上面介绍的增强LSH的方法外，有时候我们希望将多个LSH hash function得到的hash值组合起来，在此基础上得到新的hash值，这样做的好处在于减少了存储hash table的空间。下面介绍一些常用方法：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;求模运算
new hash value = old hash value % N&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;随机投影
假设通过k个LSH hash function得到了k个hash值：h1, h2…, hk。那么新的hash值采用如下公式求得：&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;new hash value = h1&lt;em&gt;r1 + h2&lt;/em&gt;r2 + … + hk*rk，其中r1, r2, …, rk是一些随机数。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;XOR异或&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;假设通过k个LSH hash function得到了k个hash值：h1, h2…, hk。那么新的hash值采用如下公式求得：&lt;/p&gt;

&lt;p&gt;new hash value = h1 XOR h2 XOR h3 … XOR hk&lt;/p&gt;

&lt;h4 id=&quot;lsh-family&quot;&gt;&lt;a href=&quot;http://infolab.stanford.edu/~ullman/mmds/ch3n.pdf&quot;&gt;LSH Family&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;d(x,y)表示x,y之间的一个距离度量，并不是所有的距离度量都有能够满足locality-sensitive的哈希函数。
通常有以下几种距离度量方式及哈希函数：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Jaccard distance,对应的LSH hash function为：minhash,其是(d1,d2,1-d1,1-d2)-sensitive的。&lt;/li&gt;
&lt;/ul&gt;

\[d(x, y) = 1 - \frac {A \intersection B} { A \union B} \\\]

&lt;ul&gt;
  &lt;li&gt;Hamming Distance：两个具有相同长度的向量中对应位置处值不同的次数,
hash function为：H(V) = 向量V的第i位上的值，其是(d1,d2,1-d1/d,1-d2/d)-sensitive&lt;/li&gt;
&lt;/ul&gt;

\[d(x, y) = \sum(x_i != y_i) \\\]

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;cosine distance,hash function为：H(V) = sign(V·R)，
R是一个随机向量。V·R可以看做是将V向R上进行投影操作。
利用随机的超平面（random hyperplane）将原始数据空间进行划分，
每一个数据被投影后会落入超平面的某一侧，经过多个随机的超平面划分后，
原始空间被划分为了很多cell，而位于每个cell内的数据被认为具有很大可能是相邻的
（即原始数据之间的cosine distance很小）。
其是(d1,d2,(180-d1)180,(180-d2)/180)-sensitive的。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Euclidean distance 是衡量D维空间中两个点之间的距离的一种距离度量方式。
对应的LSH hash function为：H(V) = |V·R + b| / a，
R是一个随机向量，a是桶宽，b是一个在[0,a]之间均匀分布的随机变量。
V·R可以看做是将V向R上进行投影操作。其是(a/2,2a,1/2,1/3)-sensitive的。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;理解：将原始数据空间中的数据投影到一条随机的直线（random line）上，
并且该直线由很多长度等于a的线段组成，
每一个数据被投影后会落入该直线上的某一个线段上（对应的桶内），
将所有数据都投影到直线上后，
位于同一个线段内的数据将被认为具有很大可能是相邻的（
即原始数据之间的Euclidean distance很小）。&lt;/p&gt;

&lt;h3 id=&quot;simhash&quot;&gt;Simhash&lt;/h3&gt;
&lt;p&gt;simhash在工业界引起很大注意力是因为google 07那篇文章，
把Simhash技术引入到海量文本去重领域。
google 通过Simhash把一篇文本映射成64bits的二进制串。下面是具体示意图：
文档每个词有个权重，同时哈希成一个二进制串。文档最终的签名是各个词签名的加权和。
&lt;strong&gt;如果两篇文档相同，则他们simhash签名汉明距离小于等于3。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/simhash_a&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通过simhash过程，将很长的文本表示为64位的二进制串，其实是在解决第二个问题，对特征进行降维。
然而，第一个问题还是没有解决。
不过，考虑到应用场景及限制–&lt;strong&gt;如果两篇文档相同，则他们simhash签名汉明距离小于等于3&lt;/strong&gt;，
作者将64位分为4份（slot)，对每份进行hash，这样的话，只有至少一个slot相同，两个文档才有可能重复。
在一定程度上降低了计算量。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/simhash_b&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;hnswhierarchcal-navigable-small-world-graphsgithub&quot;&gt;HNSW(Hierarchcal Navigable Small World graphs）(&lt;a href=&quot;https://github.com/nmslib/hnswlib&quot;&gt;github&lt;/a&gt;)&lt;/h3&gt;
&lt;p&gt;和前几种算法不同，HNSW（Hierarchcal Navigable Small World graphs）是基于图存储的数据结构。
&lt;a href=&quot;https://blog.csdn.net/u011233351/article/details/85116719&quot;&gt;参考该博客&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/hnsw_base.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设我们现在有13个2维数据向量，
我们把这些向量放在了一个平面直角坐标系内，
隐去坐标系刻度，它们的位置关系如上图所示。&lt;/p&gt;

&lt;p&gt;朴素查找法：&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;不少人脑子里都冒出过这样的朴素想法，把某些点和点之间连上线，构成一个查找图，存储备用；
当查找与粉色点最近的一点时我从任意一个黑色点出发，计算它和粉色点的距离，与这个任意黑色点有连接关系的点我们称之为“友点”（直译），
然后计算这个黑色点的所有“友点”与粉色点的距离，从所有“友点”中选出与粉色点最近的一个点，
把这个点作为下一个进入点，继续按照上面的步骤查找下去。
如果当前黑色点对粉色点的距离比所有“友点”都近，终止查找。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;朴素想法之所以叫朴素想法就是因为它的缺点非常多。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;首先，我们发现图中的K点是无法被查询到的，因为K点没有友点，怎么办？&lt;/li&gt;
  &lt;li&gt;其次，如果我们要查找距离粉色点最近的两个点，而这两个近点之间如果没有连线，
那么将大大影响效率（比如L和E点，如果L和E有连线，那么我们可以轻易用上述方法查出距离粉色点最近的两个点），怎么办？&lt;/li&gt;
  &lt;li&gt;最后一个大问题，D点真的需要这么多“友点”吗？谁是谁的友点应该怎么确定呢？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了解决上述问题，我们有如下规定：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;1）图中每个点都有“友点”: 防止某些点无法到达&lt;/li&gt;
  &lt;li&gt;2）在构图时所有距离相近（相似）到一定程度的向量必须互为友点: 近点没有连线的话，查询效率会很低&lt;/li&gt;
  &lt;li&gt;3）图中所有连线的数量最少：友点太多，会影响查找效率&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;nsw算法&quot;&gt;NSW算法&lt;/h4&gt;
&lt;p&gt;在图论中有一个很好的剖分法则专门解决上一节中提到的朴素想法的缺陷问题——德劳内（Delaunay）三角剖分算法，
这个算法可以达成如下要求：1，图中每个点都有“友点”。2，相近的点都互为“友点”。3，图中所有连接（线段）的数量最少。
效果如下图。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/nsw.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但NSW没有采用德劳内三角剖分法来构成德劳内三角网图，
原因之一是德劳内三角剖分构图算法时间复杂度太高，换句话说，构图太耗时。
原因之二是德劳内三角形的查找效率并不一定最高，
如果初始点和查找点距离很远的话我们需要进行多次跳转才能查到其临近点，
需要“高速公路”机制（Expressway mechanism, 这里指部分远点之间拥有线段连接，以便于快速查找）。
在理想状态下，我们的算法不仅要满足上面三条需求，还要算法复杂度低，同时配有高速公路机制的构图法。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/nsw_expressway.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;NSW论文中配了这样一张图，黑色是近邻点的连线，红色线就是“高速公路机制”了。
我们从enter point点进入查找，查找绿色点临近节点的时候，就可以用过红色连线“高速公路机制”快速查找到结果。&lt;/p&gt;

&lt;p&gt;那么，NSW是如何构建图结构的呢?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;向图中逐个插入点，插入一个全新点时，通过朴素想法中的朴素查找法（通过计算“友点”和待插入点的距离来判断下一个进入点是哪个点）
查找到与这个全新点最近的m个点（m由用户设置），连接全新点到m个点的连线。&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;构图算法是逐点随机插入的，这就意味着在图构建的早期，很有可能构建出“高速公路”。
假设我们现在要构成10000个点组成的图，设置m=4（每个点至少有4个“友点”），
这10000个点中有两个点，p和q，他们俩坐标完全一样。
假设在插入过程中我们分别在第10次插入p，在第9999次插入q，请问p和q谁更容易具有“高速公路”？
答：因为在第10次插入时，只见过前9个点，故只能在前9个点中选出距离最近的4个点（m=4）作为“友点”，
而q的选择就多了，前9998个点都能选，所以q的“友点”更接近q，p的早期“友点”不一定接近p，所以p更容易具有“高速公路”。
结论：一个点，越早插入就越容易形成与之相关的“高速公路”连接，越晚插入就越难形成与之相关的“高速公路”连接。
所以这个算法设计的妙处就在于扔掉德劳内三角构图法，改用“无脑添加”（NSW朴素插入算法），
降低了构图算法时间复杂度的同时还带来了数量有限的“高速公路”，加速了查找。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;NSW算法的朴素构思就讲到这里了，下面我们来说说优化。&lt;/p&gt;

&lt;p&gt;一.在查找的过程中，为了提高效率，我们可以建立一个废弃列表，
在一次查找任务中遍历过的点不再遍历。
在一次查找中，已经计算过这个点的所有友点距离查找点的距离，并且已经知道正确的跳转方向了，
这些结果是唯一的，没有必要再去做走这个路径，因为这个路径会带给我们同样的重复结果，没有意义。&lt;/p&gt;

&lt;p&gt;二.在查找过程中，为了提高准确度，我们可以建立一个动态列表，
把距离查找点最近的n个点存储在表中，并行地对这n个点进行同时计算“友点”和待查找点的距离，
在这些“友点”中选择n个点与动态列中的n个点进行并集操作，在并集中选出n个最近的友点，更新动态列表。&lt;/p&gt;

&lt;p&gt;设待查找q点的m个近邻点。使用伪代码描述如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;随机选一个点作为初始进入点，建立空废弃表g和动态列表c，g是变长的列表，c是定长为s的列表（s&amp;gt;m）,将初始点放入动态列表c（附上初始点和待查找q的距离信息），制作动态列表的影子列表c’。&lt;/li&gt;
  &lt;li&gt;对动态列表c中的所有点并行找出其“友点”，查看这些“友点”是否存储在废弃表g中，如果存在，则丢弃，如不存在，将这些剩余“友点”记录在废弃列表g中（以免后续重复查找，走冤枉路）。&lt;/li&gt;
  &lt;li&gt;并行计算这些剩余“友点”距离待查找点q的距离，将这些点及其各自的距离信息放入c。&lt;/li&gt;
  &lt;li&gt;对动态列表c去重，然后按距离排序（升序），储存前s个点及其距离信息。&lt;/li&gt;
  &lt;li&gt;查看动态列表c和c’是否一样，如果一样，结束本次查找，返回动态列表中前m个结果。如果不一样，将c’的内容更新为c的内容，执行第2步。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;跳表&quot;&gt;跳表&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/skip_list.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;hnsw&quot;&gt;HNSW&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/hnsw.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;论文中的一张示意图即可看懂作者malkov的意思。
第0层中，是数据集中的所有点，你需要设置一个常数ml，通过公式$floor(-ln(uniform(0,1)) \times ml)$来计算这个点可以深入到第几层。&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;第0层中包含图中所有节点&lt;/li&gt;
  &lt;li&gt;向上节点数依次减少，遵循指数衰减概率分布&lt;/li&gt;
  &lt;li&gt;建图时新加入的节点由指数衰减概率函数得出该点最高投影到第几层&lt;/li&gt;
  &lt;li&gt;从最高的投影层向下的层中该点均存在&lt;/li&gt;
  &lt;li&gt;搜索时从上向下依次查询&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;插入构图的时候：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;先计算这个点可以深入到第几层，&lt;/li&gt;
  &lt;li&gt;自顶层向q的层数l逼近搜索，一直到l+1,每层寻找当前层q最近邻的1个点&lt;/li&gt;
  &lt;li&gt;自l层向底层逼近搜索,每层寻找当前层q最近邻的efConstruction个点赋值到集合W&lt;/li&gt;
  &lt;li&gt;在W中选择q最近邻的M个点作为neighbors双向连接起来&lt;/li&gt;
  &lt;li&gt;检查每个neighbors的连接数，如果大于Mmax，则需要缩减连接到最近邻的Mmax个&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要注意的是第5点，每个节点的出度是有上限的，这样可以保证搜索的效率，
这里就需要针对某些边进行裁剪，那么该裁剪哪些边呢？
Fast Approximate Nearest Neighbour Graphs一文中给出了裁边的原则：&lt;/p&gt;

&lt;p&gt;edge(p1, p2) occludes edge(p1, p3) if
d(p1, p2) &amp;lt; d(p1, p3) and d(p2, p3) &amp;lt; d(p1, p3)&lt;/p&gt;

&lt;h3 id=&quot;pqproduct-quantization&quot;&gt;PQ(Product Quantization)&lt;/h3&gt;
&lt;p&gt;在介绍PQ算法前，先简要介绍vector quantization。
在信息论里，quantization是一个被充分研究的概念。
Vector quantization定义了一个量化器quantizer，即一个映射函数q，
它将一个D维向量x转换码本cookbook中的一个向量，这个码本的大小用k表示。
一般来说，码本的大小k一般会是2的幂次方，那么就可以用$log_2k$bit对应的向量来表示码本的每个值。&lt;/p&gt;

&lt;p&gt;PQ乘积量化能够加速索引的原理：即将全样本的距离计算，转化为到子空间类中心的距离计算。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/product_quantization.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设原始向量是1024维，可以把它拆解成8个子向量，每个子向量128维。
然后对每个字向量的全部50k数据分别作Kmeans计算，假设设置Kmeans的K为256。&lt;/p&gt;

&lt;p&gt;注意到每组子向量有其256个中心点，我们可以中心点的 ID 来表示每组子向量中的每个向量。
中心点的ID只需要8位来保存即可。
这样，初始一个由32位浮点数组成的1,024维向量，可以转化为8个8位整数组成。如下图&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/product_quantization_compact.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;进行向量压缩之后，距离的计算也有两种方式,一种是SDC(symmetric distance computation)，另一种是ADC(asymmetric distance computation)，
区别主要在于是否要对查询向量x做量化:
x是查询向量(query vector)，y是数据集中的某个向量，目标是要在数据集中找到x的相似向量。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/sdc_adc.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;SDC算法：先用PQ量化器对x和y表示为对应的中心点q(x)和q(y)，然后用公式1来近似d(x,y).&lt;/p&gt;

\[\hat{d}(x，y)= d(q(x),q(y)) = \sqrt{\sum_jd(q_j(x), q_j(y))^2} \tag{1}\]

&lt;p&gt;补充:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;为提高计算速度，一般会提前算好$d(c_{ji},c_{ji}′)^2$，然后在检索时就是查表，以O(1)的复杂度查出结果。&lt;/li&gt;
  &lt;li&gt;$\hat{d}(x，y)$也是$d(x，y)$的近似计算，一般会先用相似计算方法选出top N近邻，然后再做rerank以拿到最终的近邻排序结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ADC算法：只对y表示为对应的中心点q(y)，然后用下述公式2来近似d(x,y)。&lt;/p&gt;

\[\hat{d}(x，y)= d(x,q(y)) = \sqrt{\sum_jd(u_j(x), q_j(y))^2} \tag{2}\]

&lt;h3 id=&quot;倒排乘积量化ivfpq&quot;&gt;倒排乘积量化(IVFPQ)&lt;/h3&gt;

&lt;p&gt;倒排PQ乘积量化(IVFPQ)是PQ乘积量化的更进一步加速版。
brute-force搜索的方式是在全空间进行搜索，为了加快查找的速度，
几乎所有的ANN方法都是通过对全空间分割，将其分割成很多小的子空间，在搜索的时候，通过某种方式，快速锁定在某一（几）子空间，然后在该（几个）子空间里做遍历。
在上一小节可以看出，PQ乘积量化计算距离的时候，距离虽然已经预先算好了，但是对于每个样本到查询样本的距离，还是得老老实实挨个去求和相加计算距离。
但是，实际上我们感兴趣的是那些跟查询样本相近的样本，也就是说老老实实挨个相加其实做了很多的无用功，
如果能够通过某种手段快速将全局遍历锁定为感兴趣区域，则可以舍去不必要的全局计算以及排序。
倒排PQ乘积量化的”倒排“，正是这样一种思想的体现，在具体实施手段上，采用的是通过聚类的方式实现感兴趣区域的快速定位，
在倒排PQ乘积量化中，聚类可以说应用得淋漓尽致。&lt;/p&gt;

&lt;p&gt;在PQ乘积量化之前，增加了一个粗量化过程。
具体地，先对N个训练样本采用K-Means进行聚类，这里聚类的数目一般设置得不应过大，一般设置为1024差不多，
这种可以以比较快的速度完成聚类过程。
得到了聚类中心后，针对每一个样本$x_i$，找到其距离最近的类中心$c_i$后，两者相减得到样本$x_i$的残差向量($x_i-c_i$)，
后面剩下的过程，就是针对($x_i-c_i$)的PQ乘积量化过程，此过程不再赘述。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/ivfpq.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在查询的时候，通过相同的粗量化，可以快速定位到查询向量属于哪个$c_i$（即在哪一个感兴趣区域），
然后在该感兴趣区域按上面所述的PQ乘积量化距离计算方式计算距离。&lt;/p&gt;

&lt;h2 id=&quot;参考文献&quot;&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;http://yongyuan.name/blog/ann-search.html&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/138311261&lt;/li&gt;
  &lt;li&gt;http://vividfree.github.io/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/2017/08/05/understanding-product-quantization&lt;/li&gt;
  &lt;li&gt;http://www.fabwrite.com/productquantization&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/m0_37850187/article/details/92712490&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/hero_fantao/article/details/70245284&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/80552211?utm_source=wechat_session&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/u011233351/article/details/85116719&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/152522906&lt;/li&gt;
  &lt;li&gt;https://zhuanlan.zhihu.com/p/46164294&lt;/li&gt;
  &lt;li&gt;https://www.jianshu.com/p/e6af33fd8e27&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/icvpr/article/details/12342159&lt;/li&gt;
  &lt;li&gt;https://blog.csdn.net/chichoxian/article/details/80290782&lt;/li&gt;
  &lt;li&gt;https://www.cnblogs.com/wt869054461/p/8148940.html&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 20 Aug 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/08/20/%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%E7%9A%84%E7%9B%B8%E4%BC%BC%E6%A3%80%E7%B4%A2/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/08/20/%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%E7%9A%84%E7%9B%B8%E4%BC%BC%E6%A3%80%E7%B4%A2/</guid>
        
        
        <category>ann</category>
        
      </item>
    
      <item>
        <title>bilateral filter 到 HDRnet</title>
        <description>&lt;p&gt;上次介绍过了Style Transfer的一系列文章，但是可以发现，哪怕是最后一篇ADIN，也已经是2017年了。
那么，近几年大家都在做什么呢？&lt;/p&gt;

&lt;p&gt;搜索关键词的时候，发现大家都在做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Photorealistic Style Transfer&lt;/code&gt;，对目标图片的真实感有了更高的要求。&lt;/p&gt;

&lt;p&gt;正如Style Transfer的基础是&lt;a href=&quot;https://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf&quot;&gt;Image Style Transfer Using Convolutional Neural Networks&lt;/a&gt;
一样， Photorealistic Style Transfer也有其理论基础，即 Bilateral Filter （双边滤波器）。&lt;/p&gt;

&lt;h2 id=&quot;bilateral-filter&quot;&gt;Bilateral Filter&lt;/h2&gt;

&lt;p&gt;在cv领域，对图像进行滤波是常见的操作，通用公式如下：
\(I^o_p = \frac{\sum_qI(p)w(p, q)}{\sum_qw(p, q)}  \tag{1}\)&lt;/p&gt;

&lt;p&gt;这里$I(p)$代表图像，p,q是图像上的坐标，$w(p,q)$代表与位置相关的权重。
大多数滤波器的区别就在于权重的选择。&lt;/p&gt;

&lt;p&gt;以高斯滤波来说，
\(w(p,q) = G_{\sigma}(|| p - q ||) = \frac{1}{2\pi \sigma^2}exp(-\frac{|| p - q || ^ 2}{2\sigma^2})  \tag{2}\)&lt;/p&gt;

&lt;p&gt;只考虑了p,q的距离，距离越远，权重越低；这样可以用来做平滑，但是会导致边缘被摸糊掉。&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# for pixel[x][y]
new_pixel[x][y] = 0
k = 0
for i in range(-r, r + 1):
    for j in range(-r, r + 1):
        new_pixel[x][y] += pixel[i + x][j + y] * c[i][j]
        k += c[i][j]
new_pixel[x][y] /= k
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;而双边滤波则同时考虑了p,q两点的距离和像素值相似程度：
距离越近、相似程度越高，权重越大。
\(w(p,q) = G_{\sigma_s}(||p-q|)G_{\sigma_r}(|I(p) - I(q)|)  \tag{3}\)&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# for pixel[x][y]
new_pixel[x][y] = 0
k = 0
for i in range(-r, r + 1):
    for j in range(-r, r + 1):
        w = c[i][j] * s(pixel[x][y], pixel[i + x][j + y])
        new_pixel[x][y] += pixel[i + x][j + y] * w
        k += w
new_pixel[x][y] /= k
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样的话，同时考虑空域信息和灰度相似性，
达到保边去噪的目的。具有简单、非迭代、局部的特点。
但是由于保存了过多的高频信息，对于彩色图像里的高频噪声，
双边滤波器不能够干净的滤掉，只能够对于低频信息进行较好的滤波。
&lt;img src=&quot;/static/img/bilateral_filter.jpg&quot; alt=&quot;bilateral filter&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然而，在保留边缘信息的时候，双边滤波的计算量也增加了许多。&lt;/p&gt;

&lt;p&gt;有没有办法进行加速呢？&lt;/p&gt;

&lt;p&gt;考虑到高斯滤波的过程，其实和2D卷积是一样的；很显然，双边滤波的计算过程和3D卷积的计算过程也是一样的，
是不是可以借鉴一下呢？&lt;/p&gt;

&lt;p&gt;先计算出p(x,y)和q的值，offset作为第三位，像素差作为对应的值，应该也是可以的。但是仔细想想，感觉哪里不太对，
相当于卷积核在发生变化？&lt;/p&gt;

&lt;h2 id=&quot;a-fast-approximation-of-the-bilateral-filter-using-a-signal-processing-approach&quot;&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//dspace.mit.edu/bitstream/handle/1721.1/34876/MIT-CSAIL-TR-2006-073.pdf%3Fsequence%3D1&quot;&gt;A Fast Approximation of the Bilateral Filter using a Signal Processing Approach&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;虽然上述原始想法有些问题，但仔细分析后可以发现，不合理的主要原因是|I(p) - I(q)|在发生变化。
然而，再仔细思索一下，虽然值在发生变化，但是取值范围是有限的，如果第三个维度是插值的值域，就可以进行3维卷积操作了。&lt;/p&gt;

&lt;p&gt;从想法到落地，还是需要很多工作的，&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//dspace.mit.edu/bitstream/handle/1721.1/34876/MIT-CSAIL-TR-2006-073.pdf%3Fsequence%3D1&quot;&gt;Durand 在 2006 年提出了近似加速算法&lt;/a&gt;
使用接近40页的篇幅描述了加速的过程：（还没完全理解，理解后补充一下）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/fast_approximation_of_bilateral_filter.png&quot; alt=&quot;fast_bilateral filter&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如上图所示，核心思想如下：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;像素值作为一个额外维度，使得weight的计算转变成第三维度上的卷积，提高了data locality利用率&lt;/li&gt;
  &lt;li&gt;离散化，并使用一个额外的channel记录分母，称之为 homogeneous coordinate（下图右侧一列）&lt;/li&gt;
  &lt;li&gt;使用slicing操作重新得到原数据空间的值&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;承上启下的过渡-bilateral-grid&quot;&gt;承上启下的过渡： &lt;a href=&quot;https://groups.csail.mit.edu/graphics/bilagrid/bilagrid_web.pdf&quot;&gt;Bilateral Grid&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Durand 进一步发展了这个思路，和 Jiawen Chen 一起做了优秀的工作，
 将空间域和像素值域都离散化、网格化，提出了 bilateral grid 数据结构，
 进一步加速了计算过程。
 把之前引入额外维度这个动作背后的思想进一步挖掘深化，为后来的工作铺平了道路。&lt;/p&gt;

&lt;p&gt;在这个工作中，将之前提出的 fast approximate 算法中的核心思想提炼出来，
总结成一个新的数据结构 bilateral grid，并且发现很多操作都可以归纳到这个框架内。&lt;/p&gt;

&lt;p&gt;使用bilateral grid通常分为三步：
首先，从图像或者其他输入构建网格；在网格内进行变换；对网格进行切片，重构输出。
构建网格和切分网格是图像和网格空间的等价变换。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Grid Creation&lt;/p&gt;

    &lt;p&gt;给出图片I（已经归一化到$[0, 1]$之间），$s_s$表示空间采样率，
$s_r$表示取值的采样率，按照如下方法构建网格$\tau$:&lt;/p&gt;
    &lt;ul&gt;
      &lt;li&gt;初始化：对所有的网格 $(i,j,k), \tau(i,j,k)=(0,0)&lt;/li&gt;
      &lt;li&gt;填充：对于(x,y)位置的每个像素，有：&lt;/li&gt;
    &lt;/ul&gt;

\[\tau([x/s_s], [y/s_s], [I(x,y)/s_r]) += (I(x,y),1)\]

    &lt;p&gt;$[.]$表示取整（四舍五入）操作。使用$\tau = c(I)$表示构建过程。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Processing&lt;/p&gt;

    &lt;p&gt;任何接受三维输入的函数都可以用于对应的网格 ，得到新的网格$\hat{\tau} = f(\tau)$&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Extracting a 2D Map by Slicing&lt;/p&gt;

    &lt;p&gt;切片是产生分段平滑输出的关键双边网格操作。给定双边网格$\tau$和参考图片E，我们通过对
$(x/s_s, y/s_s, E(x,y)/s_r$这个位置的网格进行三线性差值来得到对应的值。
使用$M=s_E(\tau)$表示该过程。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上文提到的双边滤波，可以用如下公式表示：&lt;/p&gt;

\[bf(I) = s_I(G_{\delta_s, \delta_r} \bigotimes c(I))\]

&lt;p&gt;&lt;img src=&quot;/static/img/bilateral_grid.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;加速多种滤镜的速度bgu&quot;&gt;加速多种滤镜的速度：&lt;a href=&quot;https://people.csail.mit.edu/hasinoff/pubs/ChenEtAl16-bgu.pdf&quot;&gt;BGU&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;Chen 和 Durand 在此基础上继续发展，利用 bilateral grid 的思想，加速其他滤镜，
做出了 Bilateral Guided Upsample (BGU) 这样优秀的工作。&lt;/p&gt;

&lt;p&gt;由于很多复杂的滤镜处理速度比较慢，
一个很常用的解决思路是对原图 downsample 之后做处理，
然后用 upsample 得到处理结果。
这里 Kaiming He 的 guide filter 就是一个这种思路的很好的应用。
而在 BGU 这个例子里，利用 bilateral grid 来做 downsample - upsample 的工作，使得效果更为出色。&lt;/p&gt;

&lt;p&gt;其核心思想是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;任何滤镜效果，在局部小区域内都可以看做是一个线性变换&lt;/li&gt;
  &lt;li&gt;利用 bilateral grid 可以从一个低分辨率的图上 slice 得到高分辨率的结果&lt;/li&gt;
  &lt;li&gt;upsample 针对的是变换系数，而不是直接针对像素。这样对细节方面损失降低到最小&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/bgu.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;具体实现的步骤如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;对原图 downsample 得到一个小图&lt;/li&gt;
  &lt;li&gt;在小图上应用滤镜&lt;/li&gt;
  &lt;li&gt;在小图上划分网格（bilateral graid），拟合每一个网格中的线性变换&lt;/li&gt;
  &lt;li&gt;线性变换的系数在网格间做平滑&lt;/li&gt;
  &lt;li&gt;利用这个网格，根据原始大图在这个网格上做 slicing，得到高分辨率的线性变换系数，进一步得到高分辨率的结果&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里有两点比较重要，
一是利用 bilaeral grid 做 slicing 来 upsample，
二是用线性变换的系数做中间媒介，而不是直接 upsample 小图。
这样得到的结果更为自然，大图的细节损失很小。&lt;/p&gt;

&lt;h2 id=&quot;集大成者hdrnet&quot;&gt;集大成者：&lt;a href=&quot;https://groups.csail.mit.edu/graphics/hdrnet/data/hdrnet.pdf&quot;&gt;HDRnet&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/static/img/hdrnet.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这里重点在右上角黄色背景的这个模块，利用神经网络提取出一系列特征，
包括「低级特征」、「局部特征」和「全局特征」。
所有特征综合起来，得到一系列特征图。
从这些特征图，进一步得到每一张特征图对应的局部线性变换（参见上一小节），
并利用 bilateral grid 存下这些线性变换的系数；
注意，这里的系数对应的是 downsample 后的小图。&lt;/p&gt;

&lt;p&gt;在下面蓝色的模块中，首先得到一张「引导图」，参照这个引导图，
在 bilateral grid 中做 sliceing 操作，得到大图的 per pixel 的变换系数
（参见上一小节，对系数 upsample）。最后 apply 这些系数，就得到变换后的输出图像。&lt;/p&gt;

&lt;p&gt;这个工作有以下优点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;神经网络提取出一系列特征，包含低级特征、局部特征、全局特征。
对于图像处理来说，非常完整。
考虑到人类摄影师修图的过程，也无非考虑这几方面的特征，因此这个方法适用范围非常广。&lt;/li&gt;
  &lt;li&gt;神经网络强大的学习能力，可以学习非常复杂的变换；
局部线性变换的假设，一定程度上防止了过拟合。这个方法鲁棒性很好。&lt;/li&gt;
  &lt;li&gt;相比 BGU 在像素上做线性变换，这里是在神经网络提取的特征上做变换，结果更稳定，
并且能 handle 极为复杂、非线性极强的变换形式
（我猜测，比如可以学习某一个特定摄影师的修图风格 ← 这种非常抽象很难定义的问题）&lt;/li&gt;
  &lt;li&gt;利用 bilateral grid 做 upsample，可以得到较高的质量，
从而在前期可以放心做 downsample，
一方面减少计算量加快计算速度，一方面也防止过拟合到一些局部细节上。&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 19 Aug 2020 00:00:00 +0000</pubDate>
        <link>https://moontree.github.io/2020/08/19/%E4%BB%8Ebilateral-filter-%E5%88%B0-HDRnet/</link>
        <guid isPermaLink="true">https://moontree.github.io/2020/08/19/%E4%BB%8Ebilateral-filter-%E5%88%B0-HDRnet/</guid>
        
        
        <category>tensorflow</category>
        
        <category>cv</category>
        
        <category>style-transfer</category>
        
      </item>
    
  </channel>
</rss>
