0%

简介

从以往的实验中可以知道,神经网络中的深度是非常重要的,但是深度越深,网络就越难进行训练。存在Degradetion Problem:随着网络深度增加,训练集上的准确度可能会下降:

这说明并不是所有的网络结构都是好进行优化的。于是这篇论文提出了一种可以构建深层神经网络的结构:将原本的输入与一个浅层网络的输出相加作为最终的输出,然后将这样的结构进行堆叠。

image-20200215204852591

和直接尝试让神经网络去拟合最终期望的函数不同,这里尝试让他去拟合一个残差映射。就好比本来希望得到的函数是H(x)\mathcal{H}(x),这里我们让它去拟合一个F(x)=H(x)x\mathcal{F}(x) = \mathcal{H}(x)-x的映射,这样最终仍然可以得到原本的映射。我们假设这样可以得到相同的最终结果,并且这种结构更容易进行训练。

在极限情况下,可能identity是更优的,那么回是残差趋近于0,整个块就等同于一个非线性函数。在这里可以看做原本的堆叠之上添加了一些短路连接,但是短路连接并不会增加额外的参数和计算复杂度。所以可以认为ResNet最坏的结果只是增加了无用的层数,理论上不会使结果变得更差

论文作者在ImageNet的实验中得到了两个结论:

  1. 利用残差连接的深度网络可以很好地进行优化,而直接进行堆叠的普通网络随着层数加深可能会难以收敛
  2. 残差网络可以从额外的深度中获得提升,更深的网络可以得到更好地结果。

具体结构

对每一个基本块可以看成这样一个结构:

y=F(x,{Wi})+x\mathbf{y}=\mathcal{F}\left(\mathbf{x},\left\{W_{i}\right\}\right)+\mathbf{x}

其中F(x,{Wi})\mathcal{F}\left(\mathbf{x},\left\{W_{i}\right\}\right)代表的是要被学习的残差映射,后面是一个自映射,期中需要保证残差映射的结果和原本输入的维度是相同的,如果不相同的话,可以考虑通过一个线性的投影WsW_{s}使得维度可以match:

y=F(x,{Wi})+Wsx\mathbf{y}=\mathcal{F}\left(\mathbf{x},\left\{W_{i}\right\}\right)+W_{s} \mathbf{x}

F\mathcal{F}的形式是很多样的,但是如果只采用一层Linear,那么实际上用不用这个结构是没区别的,一般的时候是使用一个多层的卷积层来作为残差连接。

代码实现

以下是基于pytorch做的复现,其中对应的网络结构如下所示:

两层的为BasicBlock,三层的为Bottleneck:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import torch
import torch.nn as nn

class BasicBlock(nn.Module):
expansion = 1

def __init__(self, input_channel, channel, stride):
super(BasicBlock, self).__init__()

self.downsample = lambda x: x
if(input_channel != channel):
self.downsample = nn.Sequential(
nn.Conv2d(in_channels = input_channel, out_channels = channel, kernel_size = 1, stride = stride, bias = False),
nn.BatchNorm2d(channel)
)

self.relu = nn.ReLU(inplace = True)

self.convlayers = nn.Sequential(
nn.Conv2d(in_channels = input_channel, out_channels = channel, kernel_size = 3, stride = stride, padding = 1, bias = False),
nn.BatchNorm2d(channel),
nn.ReLU(inplace = True),
nn.Conv2d(in_channels = channel, out_channels = channel, kernel_size = 3, stride = 1, padding = 1, bias = False),
nn.BatchNorm2d(channel)
)
def forward(self, x):
out = self.downsample(x) + self.convlayers(x)
out = self.relu(out)
return out

class Bottleneck(nn.Module):
expansion = 4

def __init__(self, input_channel, channel, stride, expansion = 4):
super(Bottleneck, self).__init__()
self.expansion = expansion
output_channel = channel * expansion

self.downsample = lambda x: x
if(input_channel != output_channel):
self.downsample = nn.Sequential(
nn.Conv2d(in_channels = input_channel, out_channels = output_channel, kernel_size = 1, stride = stride, bias = False),
nn.BatchNorm2d(output_channel)
)

self.relu = nn.ReLU(inplace = True)

self.convlayers = nn.Sequential(
nn.Conv2d(in_channels = input_channel, out_channels = channel, kernel_size = 1, stride = 1, bias = False),
nn.BatchNorm2d(channel),
nn.ReLU(inplace = True),
nn.Conv2d(in_channels = channel, out_channels = channel, kernel_size = 3, stride = stride, padding = 1, bias = False),
nn.BatchNorm2d(channel),
nn.ReLU(inplace = True),
nn.Conv2d(in_channels = channel, out_channels = output_channel, kernel_size = 1, stride = 1, bias = False),
nn.BatchNorm2d(output_channel)
)
def forward(self, x):
out = self.downsample(x) + self.convlayers(x)
out = self.relu(out)
return out


class ResNet(nn.Module):
def __init__(self, block, block_nums, input_channel, class_num):
super(ResNet, self).__init__()

self.stacklayers = nn.Sequential(
nn.Conv2d(in_channels = input_channel, out_channels = 64, kernel_size = 7, stride = 2, padding = 3, bias = False),
nn.BatchNorm2d(64),
nn.ReLU(inplace = True),
nn.MaxPool2d(kernel_size = 3, stride = 2),
self.make_layers(block = block, input_channel = 64, channel = 64, stride = 1, block_num = block_nums[0]),
self.make_layers(block = block, input_channel = 64 * block.expansion, channel = 128, stride = 2, block_num = block_nums[1]),
self.make_layers(block = block, input_channel = 128 * block.expansion, channel = 256, stride = 2, block_num = block_nums[2]),
self.make_layers(block = block, input_channel = 256 * block.expansion, channel = 512, stride = 2, block_num = block_nums[3]),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(512*block.expansion, class_num)
)

def make_layers(self, block, input_channel, channel, stride, block_num, expansion = 4, reduction = 16):
layers = []
layers.append(block(input_channel, channel, stride))
input_channel = channel * block.expansion
for _ in range(1, block_num):
layers.append(block(input_channel, channel, 1))
return nn.Sequential(*layers)

def forward(self, x):
out = self.stacklayers(x)
return out

def ResNet_18(input_channel, class_num = 1000):
return ResNet(BasicBlock, [2,2,2,2], input_channel, class_num)

def ResNet_34(input_channel, class_num = 1000):
return ResNet(BasicBlock, [3,4,6,3], input_channel, class_num)

def ResNet_50(input_channel, class_num = 1000):
return ResNet(Bottleneck, [3,4,6,3], input_channel, class_num)

def ResNet_101(input_channel, class_num = 1000):
return ResNet(Bottleneck, [3,4,23,3], input_channel, class_num)

def ResNet_152(input_channel, class_num = 1000):
return ResNet(Bottleneck, [3,8,36,3], input_channel, class_num)

Gumbel-Softmax Distribution

考虑zz是一个定类型的变量,对于每个类型有着概率π1,π2,,πk\pi_1,\pi_2,\ldots,\pi_k。考虑到从这个概率分布中的采样可以用一个onehot向量来表示,当数据量很大的时候满足:

Ep[z]=[π1,,πk]\mathbb{E}_p[z]=[\pi_1,\ldots,\pi_k]

Gumbel-Max trick 提供了一个简单且高效的来对符合π\pi这样概率分布的zz进行采样的方法:

z=onehot(argmaxi[gi+logπi])z = \text{onehot} \left(\arg \max_i [g_i+\log \pi_i]\right)

其中gig_i是从Gumbel(0,1)中独立采出的,它可以利用Uniform(0,1)中的采样来计算得到:

uUniform(0,1)g=log(log(u)).\begin{aligned} u &\sim \text{Uniform}(0,1) \\ g &= -\log(-\log(u)). \end{aligned}

之后利用softmax来获得一个连续可导对argmax的估计

yi=exp((log(πi)+gi)/τ)j=1kexp((log(πj)+gj)/τ)for i=1,,k\begin{aligned} y_{i}=\frac {\exp \left(\left(\log \left(\pi_{i}\right)+g_{i}\right) / \tau\right)} {\sum_{j=1}^{k} \exp \left(\left(\log \left(\pi_{j}\right)+g_{j}\right) / \tau\right)}\quad \text{for} \ i=1, \ldots, k \end{aligned}

Gumbel-Softmax分布的概率密度如下表是:

pπ,τ(y1,,yk)=Γ(k)τk1(i=1kπi/yiτ)ki=1k(πi/yiτ+1)p_{\pi, \tau}\left(y_{1}, \ldots, y_{k}\right)=\Gamma(k) \tau^{k-1}\left(\sum_{i=1}^{k} \pi_{i} / y_{i}^{\tau}\right)^{-k} \prod_{i=1}^{k}\left(\pi_{i} / y_{i}^{\tau+1}\right)

可以知道对于温度τ\tau而言,越接近于零,那么从Gumbel-Softmax分布中的采样就越接近onehot并且Gumbel-Softmax分布同原始的分布p(z)p(z)也更加的相似。

Gumbel-Softmax Estimator

可以发现对于任意的τ>0\tau>0,Gumbel-Softmax分布都是光滑的,可以求出偏导数y/π\partial y / \partial \pi对参数π\pi。于是用Gumbel-Softmax采样来代替原有的分类采样,就可以利用反向传播来计算梯度了。

对于学习过程中来说,实际上存在一个tradeoff。当τ\tau较小的时候,得到的sample比较接近onehot但是梯度的方差很大,当τ\tau较大的时候,梯度的方差比较小但是得到的sample更平滑。在实际的操作中,我们通常从一个较高的τ\tau开始,然后逐渐退火到一个很小的τ\tau。事实上,对于很多种的退火方法,结果都表现的不错。

Straight-Through Gumbel-Softmax Estimator

对于有些任务需要严格的将其限制为得到的就是离散的值,那么这个时候可以考虑对于yy来做一个arg max得到zz,在反向传播的时候利用θzθy\nabla_\theta z \approx \nabla_\theta y来进行梯度的估计。

即通过离散的方式进行采样,但是从连续的路径进行求导。这个叫做ST Gumbel-Softmax estimator,可以知道,当温度τ\tau较高的时候,这依然可以采样得到离散的采样值。

主要总结了一些随机神经网络训练的方法,进行了一个对比。

上图中

  1. 正常的无随机节点的梯度下降
  2. 存在随机节点的时候,梯度在这个地方不能很好地进行反传
  3. 采用log trick绕开随机节点传递梯度
  4. 估计梯度进行传播,例如前文提到的ST Estimator
  5. 采用重参数化方法,就是这里的Gumbel-Softmax Estimator

Semi-Supervised Generative Models

对于重参数化和log trick就不再多说,这里看一个半监督生成模型的推断。

考虑到一个半监督网络,从带标签数据(x,y)DL(x,y)\sim\mathcal{D}_L和不带标签数据xDUx\sim \mathcal{D}_U中进行学习。

有一个分辨网络(D)qϕ(yx)q_\phi(y|x),一个推断网络(I)qϕ(zx,y)q_\phi(z|x,y),和一个生成网络(G)pθ(xy,z)p_\theta(x|y,z),通过最大化生成网络输出的log似然的变分下界来进训练。

对于带标签的数据,y是观测到的结果,所以变分下界如下:

logpθ(x,y)L(x,y)=Ezqϕ(zx,y)[logpθ(xy,z)]KL[qϕ(zx,y)pθ(y)p(z)]\begin{aligned} \log p_\theta(x,y) &\ge \mathcal{L}(x,y)\\ &= \mathbb{E}_{z \sim q_\phi(z|x,y)}[\log p_\theta(x|y,z)] - KL[q_\phi(z|x,y)||p_\theta(y)p(z)] \end{aligned}

对于无标签数据,重点在于对于离散的分布没有办法进行重参数化,所以这里采用的方法是对于margin out所有类别的y,同样是在qϕ(zx,y)q_\phi(z|x,y)上面进行推断,得到的变分下界如下所示(有一说一我推的和论文不一样,但我觉得论文里面的公式写错了):

logpθ(x)U(x)=Ezqϕ(y,zx)[logpθ(xy,z)+logpθ(y)+logp(z)logqϕ(y,zx)]=Ezqϕ(y,zx)[logpθ(xy,z)logqϕ(zx,y)pθ(y)p(z)+logqϕ(zx,y)qϕ(y,zx)]=Ezqϕ(y,zx)[logpθ(xy,z)logqϕ(zx,y)pθ(y)p(z)+log1qϕ(yx)]=yqϕ(yx)Ezqϕ(zx,y)[logpθ(xy,z)logqϕ(zx,y)pθ(y)p(z)+log1qϕ(yx)]=yqϕ(yx)Ezqϕ(zx,y)[logpθ(xy,z)logqϕ(zx,y)pθ(y)p(z)]+yqϕ(yx)log1qϕ(yx)=yqϕ(yx)L(x,y)+H(qϕ(yx))\begin{aligned}\log p_{\theta}(x) &\geq\mathcal{U}(x) \\&=\mathbb{E}_{z \sim q_{\phi}(y, z | x)}\left[\log p_{\theta}(x | y, z)+\log p_{\theta}(y)+\log p(z)-\log q_{\phi}(y, z | x)\right] \\&=\mathbb{E}_{z \sim q_{\phi}(y, z | x)}\left[\log p_{\theta}(x | y, z)-\log \frac{q_\phi(z|x,y)}{p_{\theta}(y) p(z)} + \log \frac{q_\phi(z|x,y)}{q_\phi(y,z|x)}\right]\\&=\mathbb{E}_{z \sim q_{\phi}(y, z | x)}\left[\log p_{\theta}(x | y, z)-\log \frac{q_\phi(z|x,y)}{p_{\theta}(y) p(z)} + \log \frac{1}{q_\phi(y|x)}\right]\\&=\sum_{y} q_\phi(y|x)\mathbb{E}_{z \sim q_{\phi}(z | x,y)}\left[\log p_{\theta}(x | y, z)-\log \frac{q_\phi(z|x,y)}{p_{\theta}(y) p(z)} + \log \frac{1}{q_\phi(y|x)}\right]\\&=\sum_{y} q_\phi(y|x)\mathbb{E}_{z \sim q_{\phi}(z | x,y)}\left[\log p_{\theta}(x | y, z)-\log \frac{q_\phi(z|x,y)}{p_{\theta}(y) p(z)}\right] + \sum_{y} q_\phi(y|x)\log \frac{1}{q_\phi(y|x)}\\&=\sum_{y} q_{\phi}(y | x)\mathcal{L}(x, y)+\mathcal{H}\left(q_{\phi}(y | x)\right)\end{aligned}

最终得到的最大化目标为下面这个式子:

J=E(x,y)DL[L(x,y)]+ExDU[U(x)]+αE(x,y)DL[logqϕ(yx)]\mathcal{J}=\mathbb{E}_{(x, y) \sim \mathcal{D}_{L}}[\mathcal{L}(x, y)]+\mathbb{E}_{x \sim \mathcal{D}_{U}}[\mathcal{U}(x)]+\alpha \cdot \mathbb{E}_{(x, y) \sim \mathcal{D}_{L}}\left[\log q_{\phi}(y | x)\right]

容易发现,前两项一个是针对带标签数据的变分下界最大化,一个是针对无标签数据的最大化,最后一项代表分辨网络的对数似然,其中α\alpha参数越大,说明越看重分辨网络的能力。是一个在分辨网络和生成网络之间进行tradeoff的参数。

对于这种方法,假设要margin out一共k个类别,那么对每个前向/反向步需要O(D+k(I+G))\mathcal{O}(D+k(I+G)),但是采用Gumbel-Softmax方法进行重参数化,就可以直接进行反向传播而不需要margin out,时间复杂度降低到了O(D+I+G)\mathcal{O}(D+I+G),在类别很多的情况下可以有效降低训练的时间复杂度!

准备面试的时候找到了两套C++面试题,做了一下,解答直接写在上面了,放在这里分享出来。感觉难度并不是很高,这是第二套~


A. Show me the code

1. Please create a “String” class to process char strings. This string class should satisfy the following requirements:

  • It could be used as an original type like “int” to define variables and support assignment and copy.
  • It could be used as the parameter type of a function and the return value type of a function.
  • It could be used as the element type of the STL container, e.g., vector/list/deque.

In other words, your “String” class could be used in the following code and be complied successfully by a standard C++ compiler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void foo(String x)
{
}
void bar(const String& x)
{
}
String baz()
{
String ret("world");
return ret;
}
int main()
{
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s4 = baz();
std::vector<String> svec;
svec.push_back(s0);
svec.push_back(s1);
svec.push_back(baz());
svec.push_back("good job");
}

略,感觉以前写过。

2. Imagine we have a single linked list, please write a function which could remove a specified node in the list.

  • Please define the data structure for the list as you need.
  • Pass the head pointer to the function.
  • Pass the pointer to the node to be removed to the function. Remove the node in the list if it has the same pointer value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct ListNode{
int val;
ListNode* next;
ListNode(int _val): val(_val){}
};

ListNode* deleteNode(ListNode* head, int val)
{
if(!head)
return NULL;
while(head->val == val)
{
head = head->next;
}
if(!head)
return NULL;
ListNode* pnode = head->next;
ListNode* prev = head;
while(pnode)
{
if(pnode->val == val)
{
prev->next = pnode->next;
pnode = pnode->next;
}
else
{
prev = pnode;
pnode = pnode->next;
}
}
return head;
}

B. Basic Part

1. What are wrong in the following code? Please point out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main()
{
char a = 'a';
int b = 0;
int *pInt1 = &b;
int c = *pInt1;
pInt1 = (int*)(&a);
int *pInt2 = pInt1 + 1;
int d = *pInt2;
void *pV = &a;
// char * pV = &a;
pV++; // 对空指针不能最算术运算
char e = *pV;
}

2. What are wrong in the following code? Please provide your FIX.

Common.h

1
2
3
4
5
int var1;
void foo(int input)
{
// some code
}

TestA.h

1
2
3
4
5
6
7
8
#include "Common.h"
#include "TestB.h"

class CTestA
{
private:
CTestB m_b;
};

TestB.h

1
2
3
4
5
6
7
8
#include "Common.h"
#include "TestA.h"

class CTestB
{
private:
CTestA m_a;
};

TestA.cpp

1
2
#include "TestA.h"
// some code

TestB.cpp

1
2
#include "TestB.h"
// some code

提前声明,结构体内部都采用指针而不是实体。


C. STL

1. Errors, inefficiency and potential bugs exsit in the following code, please point them out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int foo(std::map<int, int>& mapIn, std::set<int>& setIn)
{
std::vector<int> va(10);
std::vector<int> vb;
std::copy(va.begin(), va.end(), vb.begin());

//std::vector<int> vb(va);

std::vector<int> vc(100);
auto iter = va.begin() + 5;
int varInt = *iter;
va.push_back(vc.begin(), vc.end());
varInt = *(++iter);
if (mapIn[4] == 0)
{
// do something
}
auto itVec = std::find(vc.begin(), vc.end(), 100);
if (itVec != vc.end())
{
// do something
}

//auto itSet = setIn.find(10);
//Set本来就是有序的结构,利用可以进行二分查找,效率更高

auto itSet = std::find(setIn.begin(), setIn.end(), 10);
if (itSet == setIn.end())
{
// do something
}
}

2. Please see the following code, TypeA could be either a function pointer or a functor, please try to provide the definition for TypeA in both function pointer way and functor way.

1
2
3
4
5
6
void foo(TypeA processor)
{
int paraInt = 0;
const char* pParaStr = "Test";
int rtn = processor(paraInt, pParaStr);
}

函数指针:

1
2
3
4
5
int function_pointer(int pInt, const char* pStr)
{
//do something
return 0;
}

函数对象:

1
2
3
4
5
6
7
class functor{
public:
int operator ()(int pInt, const char * pStr){
// do something
return 0;
}
};

准备面试的时候找到了两套C++面试题,做了一下,解答直接写在上面了,放在这里分享出来。感觉难度并不是很高,这是第一套~


For your answers, either Chinese or English are OK.

A. Coding

1. Please create a class to represent Int. Only declaration is required. Please give the declaration of all member functions, friend functions, and member variables.

2. Imagine we have a single linked list, please write a function which could move a specified node to the tail of the list.

  • Please define the data structure for the list as you need.
  • Pass the head pointer to the function.
  • Pass the pointer of the node to be moved to the function. Move the node to the tail of the list if it has the same pointer value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct ListNode{
int val;
ListNode* next;
ListNode(int _val): val(_val){}
};

ListNode* deleteNode(ListNode* head, int val)
{
if(!head)
return NULL;
while(head->val == val)
{
head = head->next;
}
if(!head)
return NULL;
ListNode* pnode = head->next;
ListNode* prev = head;
while(pnode)
{
if(pnode->val == val)
{
prev->next = pnode->next;
pnode = pnode->next;
}
else
{
prev = pnode;
pnode = pnode->next;
}
}
return head;
}

B. Read and Answer

1. What are wrong in the following code?

1
2
3
4
5
6
7
8
void foo(short startIndex)
{
short buffer[50000] = { 0 };
for (auto i = startIndex; i < 40000; ++i)
{
buffer[i] = (char)i;
}
}

Because i is short, i < 40000 is always True, the loop NEVER ends.

2. Mark all lines which are wrong and provide the FIX.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class CBase
{
public:
CBase()
: m_pVar1(NULL)
{
m_pVar2 = new int(4);
}
~CBase()
{
delete m_pVar2;
delete m_pVar1; // Can't delete NULL pointer
/*
*if(! m_pVar1)
* delete m_pVal1;
*/
}
void Init(int* pVar)
{
m_pVar1 = pVar;
}
private:
int* m_pVar1;
int* m_pVar2;
}
class CDerive : public CBase
{
public:
CDerive(int var) : m_var(var) {};
//CDerive(){}
~CDerive() {};
private:
int m_var;
}
int main()
{
CDerive* pDerives = new CDerive[10]; // can't init
int *pVar = new int(10);
for (int i = 0; i < 10; ++i)
{
pDerive[i].Init(pVar);
}
delete pDerives; // pDerives is an array
//delete[] pDerives;
delete pVar;
}

3. The following code could not be compiled, give TWO ways to fix it.

1
2
3
4
5
6
7
8
9
10
11
12
13
class CFoo
{
public:
CFoo() : m_var(0) {};
~CFoo() {};
bool AddAndCheck() const
{
m_var += 5;
return m_var < 10;
}
private:
int m_var;
};

第一种:丢掉const

1
2
3
4
5
6
7
8
9
10
11
12
13
class CFoo
{
public:
CFoo() : m_var(0) {};
~CFoo() {};
bool AddAndCheck()// drop the const
{
m_var += 5;
return m_var < 10;
}
private:
int m_var;
};

第二种:加上mutable

1
2
3
4
5
6
7
8
9
10
11
12
13
class CFoo
{
public:
CFoo() : m_var(0) {};
~CFoo() {};
bool AddAndCheck() const
{
m_var += 5;
return m_var < 10;
}
private:
mutable int m_var;
};

4. What are wrong in the following code? Provide your fix.

1
2
3
4
5
6
7
8
#define INCREASE(a) ++a 
// #define INCREASE(a) a+1
void foo()
{
int a = 3;
int b = 7;
cout<<INCREASE(a * b)<<endl; // 等价于(++a) * b
}

5. What are wrong in the following code? Why?

1
2
3
4
5
char* GetStaticBuffer()
{
char buffer[100] = { 0 };
return buffer;
}

buffer在栈上分配的,返回之后释放了。

6. With default configuration, in the following code, function fooA is OK but fooB has issues, why?

1
2
3
4
5
6
7
8
9
const int TEN_MEGA_BYTES = 10 * 1024 * 1024;
void fooA()
{
char *pBuffer = new char[TEN_MEGA_BYTES];
}
void fooB()
{
char buffer[TEN_MEGA_BYTES] = { 0 };
}

fooA()在堆上进行分配,fooB()在栈上进行分配,栈小,会爆。

7. In the following code, line 32, I want to give “student_teacher” as the input parameter, but by mistake, I typed “student”. The compiling succeeded anyway. Why? If I want the compiling be failed in this condition, how to do?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CStudent
{
public:
CStudent() {};
~CStudent() {};
};

class CTeacher
{
public:
CTeacher() {};
// delete this part------------
CTeacher(CStudent student)
: m_student(student)
{
}
~CTeacher() {};
// end here -------------------
void Teach() {};
private:
CStudent m_student;
};

void foo(CTeacher teacher)
{
teacher.Teach();
}
int main()
{
CStudent student;
CTeacher student_teacher;
foo(student);
}

8. What is the output of the following code?

1
2
3
4
5
6
7
8
9
10
11
12
13
int fooVar = 10;
void foo(int *pVar, int& var)
{
pVar = &fooVar;
var = fooVar;
}
int main()
{
int var1 = 1;
int var2 = 2;
foo(&var1, var2);
cout<<var1<<":"<<var2<<endl;
}

1:10

9. How to run some code before main function is called?

1
2
3
4
5
6
7
8
9
10
11
12
13
int code_before_main()
{
cout<<"code before main()"<<endl;
return 0;
}

int useless = code_before_main();

int main()
{
cout<<"main() start"<<endl;
}

10. What the difference are between

std::vector::resize() and std::vector::reserve()

std::vector::size() and std::vector::capacity()

size()对应的是有效空间,而capacity()对应的是实际空间。

resize()调整的是有效空间的大小,reserve()调整的是实际空间大小。

11. Template greater<> is often used when operating the STL containers. Please give an implementation of greater<>.

Hints: Two template parameters. One is a number to be compared as base, another is the element to be compared.

1
2
3
4
5
template <class T> 
struct greater{
bool operator() (const T& x, const T& y) const
{return x>y;}
};

12. Say we have a class named Printer which has a member function as Print(). Now there’s a container as vector. Please give a lambda expression which could be used in for_each() to call Print() function for every instance in vector.

1
for_each(v.begin(), v.end(), [](const T& n) { Printer().Print(n); });

2019

无所得

有所失

先回顾一下全贝叶斯推断:

训练阶段:

p(θXtr,Ytr)=p(YtrXtr,θ)p(θ)p(YtrXtr,θ)p(θ)dθp(\theta|X_{tr},Y_{tr}) =\frac{p(Y_{tr}|X_{tr},\theta)p(\theta)}{\color{red}{\int p(Y_{tr}|X_{tr},\theta)p(\theta) d\theta}}

测试阶段:

p(yx,Xtr,Ytr)=p(yx,θ)p(θXtr,Ytr)dθp(y|x,X_{tr},Y_{tr}) =\color{red}{\int p(y|x,\theta) p(\theta|X_{tr},Y_{tr})d\theta }

红色分布难以计算,使得后验分布只有在面对简单的共轭模型才能被精确求解

Approximate inference

概率模型:p(x,θ)=p(xθ)p(θ)p(x,\theta)=p(x|\theta)p(\theta)

变分推断(Variational Inference):

采用一个简单的分布来近似后验:p(θx)q(θ)Qp(\theta|x)\approx q(\theta)\in \mathcal{Q}

  • 有偏的
  • 高效并且可拓展

蒙特卡洛方法(MCMC):

从没有标准化的p(θx)p(\theta|x)里面进行采样(因为下面归一化的部分难以计算):

  • 无偏的
  • 需要很多的采样才能近似

Variational inference

logp(x)=q(θ)logp(x)dθ=q(θ)logp(x,θ)p(θx)dθ=q(θ)logp(x,θ)q(θ)p(θx)q(θ)dθ=q(θ)logp(x,θ)q(θ)dθ+q(θ)logq(θ)p(θx)dθ=L(q(θ))+KL(q(θ)p(θx))\begin{aligned} \log p(x) &= \int q(\theta)\log p(x) d\theta =\int q(\theta)\log \frac{p(x,\theta)}{p(\theta|x)}d\theta\\ &=\int q(\theta)\log \frac{p(x,\theta)q(\theta)}{p(\theta|x)q(\theta)}d\theta\\ &=\int q(\theta)\log \frac{p(x,\theta)}{q(\theta)}d\theta + \int q(\theta)\log\frac{q(\theta)}{p(\theta|x)}d\theta\\ &=\color{green}{\mathcal{L}(q(\theta))}+ \color{red}{KL(q(\theta)||p(\theta|x))} \end{aligned}

前面的绿色部分是ELBO(Evidence lower bound)

后面的红色部分是用于变分推断的KL散度,KL散度越小,说明我们的估计与后验分布越接近

但是后验分布是未知的,否则就不需要求解了,再看一遍上面这个公式:

logp(x)=L(q(θ))+KL(q(θ)p(θx))\log p(x) = \mathcal{L}(q(\theta)) + KL(q(\theta)||p(\theta|x))

可以发现前面logp(x)\log p(x)q(θ)q(\theta)是没有关系的,那么要最小化KL散度,实际上就相当于最大化ELBO:

KL(q(θ)p(θx))minq(θ)QL(q(θ))maxq(θ)QKL(q(\theta)||p(\theta|x))\rightarrow\min_{q(\theta)\in \mathcal{Q}} \quad \Leftrightarrow \quad \mathcal{L}(q(\theta))\rightarrow\max_{q(\theta)\in\mathcal{Q}}

改写一遍变分下界:

L(q(θ))=q(θ)logp(x,θ)q(θ)dθ=q(θ)logp(xθ)p(θ)q(θ)dθ=Eq(θ)logp(xθ)KL(q(θ)p(θ))\begin{aligned}\mathcal{L}(q(\theta))&=\int q(\theta)\log\frac{p(x,\theta)}{q(\theta)}d\theta\\ &=\int q(\theta)\log\frac{p(x|\theta)p(\theta)}{q(\theta)}d\theta\\ &= \color{green}{\mathbb{E}_{q(\theta)}\log p(x|\theta) } - \color{red}{KL(q(\theta)||p(\theta))} \end{aligned}

前面绿色的为数据项,后面红色的为正则项

最终的优化问题就在于:

L(q(θ))=q(θ)logp(x,θ)q(θ)dθmaxq(θ)Q\mathcal{L}(q(\theta)) = \int q(\theta) \log \frac{p(x,\theta)}{q(\theta)}d\theta \quad \rightarrow \quad \max_{q(\theta)\in \mathcal{Q}}

问题的关键是,怎么对于一个概率分布进行最优化

Mean Field Variational Inference

Mean Field Approximation

L(q(θ))=q(θ)logp(x,θ)q(θ)dθmaxq(θ)=q1(θ1)qm(θm)\mathcal{L}(q(\theta)) = \int q(\theta)\log \frac{p(x,\theta)}{q(\theta)}d\theta \quad \rightarrow \quad \max_{q(\theta)=q_1(\theta_1)\cdot \ldots \cdot q_m(\theta_m)}

块坐标上升法(Block coordinate assent):

每次都固定除了一个维度分布其他的部分{qi(θi)}ij\{q_i(\theta_i)\}_{i\ne j},然后对一个维度上的分布进行优化

L(q(θ))maxqj(θj)\mathcal{L}(q(\theta)) \quad \rightarrow \quad \max_{q_j(\theta_j)}

由于除了qj(θj)q_j(\theta_j)其他维度都是固定的,可以得到如下的数学推导:

L(q(θ))=q(θ)logp(x,θ)q(θ)=Eq(θ)logp(x,θ)Eq(θ)logq(θ)=Eq(θ)logp(x,θ)k=1mEqk(θk)logqk(θk)=Eqj(θj)[Eqijlogp(x,θ)]Eqj(θj)logqj(θj)+Const{rj(θj)=1Zjexp(Eqijlogp(x,θ))}=Eqj(θj)logrj(θj)qj(θj)+Const=KL(qj(θj)rj(θj))+Const\begin{aligned} \mathcal{L}(q(\theta)) &=\int q(\theta)\log \frac{p(x,\theta)}{q(\theta)}\\ &=\mathbb{E}_{q(\theta)}\log p(x,\theta) - \mathbb{E}_{q(\theta)}\log q(\theta)\\ &=\mathbb{E}_{q(\theta)}\log p(x,\theta) - \sum_{k=1}^m \mathbb{E}_{q_k(\theta_k)}\log q_k(\theta_k)\\ &=\mathbb{E}_{q_j(\theta_j)}\left[\mathbb{E}_{q_{i\ne j}}\log p(x,\theta)\right] - \mathbb{E}_{q_j(\theta_j)}\log q_j(\theta_j)+Const\\ &\left\{r_j(\theta_j) = \frac{1}{Z_j} \exp\left(\mathbb{E}_{q_{i\ne j}}\log p(x,\theta)\right)\right\}\\ &=\mathbb{E}_{q_j(\theta_j)} \log \frac{r_j(\theta_j)}{q_j(\theta_j)}+Const\\ &=-KL\left(q_j(\theta_j)||r_j(\theta_j)\right)+Const \end{aligned}

在块坐标下降中的每一步优化问题转化为了:

L(q(θ))=KL(qj(θj)rj(θj))+Constmaxqj(θj)\mathcal{L}(q(\theta)) = -KL(q_j(\theta_j)||r_j(\theta_j)) + Const \quad \rightarrow \quad \max_{q_j(\theta_j)}

实际上就是要最小化KL散度,容易发现解为:

qj(θj)=rj(θj)=1Zjexp(Eqijlogp(x,θ))q_j(\theta_j) = r_j(\theta_j) =\frac{1}{Z_j} \exp\left(\mathbb{E}_{q_{i\ne j}}\log p(x,\theta)\right)

Parametric variational inference

考虑对于变分分布的参数族:

q(θ)=q(θλ)q(\theta) = q(\theta|\lambda)

限制在于,我们选择了一族固定的分布形式:

  • 如果选择的形式过于简单,我们可能不能有效地建模数据
  • 如果选择的形式足够复杂,我们不能保证把它训得很好来拟合数据

但这样就把变分推断就转变成了一个参数优化问题:

L(q(θλ))=q(θλ)logp(x,θ)q(θλ)dθmaxλ\mathcal{L}(q(\theta|\lambda)) = \int q(\theta|\lambda) \log\frac{p(x,\theta)}{q(\theta|\lambda)}d\theta \quad \rightarrow \quad \max_{\lambda}

只要我们能够计算变分下界(ELBO)对于θ\theta的导数,那么就可以使用数值优化方法来对这个优化问题进行求解

Summary

Full Bayesian inference p(θx)p(\theta\mid x)
MP inference p(θx)δ(θθMP)p(\theta\mid x)\approx\delta(\theta-\theta_{MP})
Mean field variational inference p(θx)q(θ)=j=1mqj(θj)p(\theta\mid x)\approx q(\theta)=\prod_{j=1}^m q_j(\theta_j)
Parametric variational inference p(θx)q(θ)=q(θλ)p(\theta\mid x)\approx q(\theta)=q(\theta\mid\lambda)

Conditional=JointMarginal,p(xy)=p(x,y)p(y)\text{Conditional} = \frac{\text{Joint}}{\text{Marginal}},\quad p(x|y)=\frac{p(x,y)}{p(y)}

Product Rule

联合分布可以被表示为一维的条件分布的乘积

p(x,y,z)=p(xy,z)p(yz)p(z)p(x,y,z)=p(x|y,z)p(y|z)p(z)

Sum Rule

任何边缘分布可以利用联合分布通过积分得到

p(y)=p(x,y)dxp(y) = \int p(x,y)dx

Bayes理论

p(yx)=p(x,y)p(x)=p(xy)p(y)p(x)=p(xy)p(y)p(xy)p(y)dyp(y|x) = \frac{p(x,y)}{p(x)} = \frac{p(x|y)p(y)}{p(x)}=\frac{p(x|y)p(y)}{\int p(x|y)p(y)dy}

Bayes理论定义了当新的信息到来的时候,可能性的改变:

Posterior=Likelihood×PriorEvidence\text{Posterior} = \frac{\text{Likelihood} \times \text{Prior}}{\text{Evidence}}

统计推断(Statistical inference)

问题描述:

给定从分布p(xθ)p(x|\theta)中得到的独立同分布变量X=(x1,,xn)X = (x_1,\ldots,x_n),来估计θ\theta

常规方法:

采用极大似然估计(maximum likelihood estimation)

θML=argmaxp(Xθ)=argmaxi=1np(xiθ)=argmaxi=1n=logp(xiθ)\theta_{ML} = \arg \max p(X|\theta) = \arg \max \prod_{i=1}^{n}p(x_i|\theta) = \arg \max \sum_{i=1}^{n} = \log p(x_i|\theta)

贝叶斯方法:

用先验p(θ)p(\theta)来编码θ\theta的不确定性,然后采用贝叶斯推断

p(θX)=i=1np(xiθ)p(θ)i=1np(xiθ)p(θ)dθp(\theta|X) = \frac{\prod_{i=1}^{n}p(x_i|\theta)p(\theta)} {\int \prod_{i=1}^{n}p(x_i|\theta)p(\theta)d\theta}

频率学派vs. 贝叶斯学派

频率学派 贝叶斯学派
变量 有随机变量也有确定的 全都是随机变量
适用范围 n>>dn>>d n\forall n
  • 现代机器学习模型中可训练的参数数量已经接近训练数据的大小了

  • 频率学派得到的结果实际上是一种受限制的Bayesian方法:

    limn/dp(θx1,,xn)=δ(θθML)\lim_{n/d \rightarrow \infty}p(\theta|x_1,\ldots,x_n)=\delta(\theta-\theta_{ML})

    注:此处的δ\delta函数指的是狄拉克函数

贝叶斯方法的优点

  • 可以用先验分布来编码我们的先验知识或者是希望的做种结果
  • 先验是一种正则化的方式
  • 相对于θ\theta的点估计方法,后验还包含有关于估计的不确定性关系的信息

概率机器学习模型

数据:

  • xx – 观察到变量的集合(features)
  • yy – 隐变量的集合(class label / hidden representation, etc.)

模型:

  • θ\theta – 模型的参数(weights)

Discriminative probabilistic ML model

通常假设θ\theta的先验与xx没有关系

p(y,θx)=p(yx,θ)p(θ)p(y,\theta|x) = p(y|x,\theta)p(\theta)

Examples:

  • 分类或者回归任务(隐层表示比观测空间简单得多)
  • 机器翻译(隐层表示和观测的空间有着相同的复杂度)

Generative probabilistic ML model

可能会很难训练,因为通常而言观测到的xx会比隐层复杂很多。

Examples:

  • 文本,图片的生成

贝叶斯机器学习模型的训练与预测

Training阶段:θ\theta上的贝叶斯推断

p(θXtr,Ytr)=p(YtrXtr,θ)p(θ)p(YtrXtr,θ)p(θ)dθp(\theta|X_{tr},Y_{tr}) = \frac{p(Y_{tr}|X_{tr},\theta)p(\theta)} {\int p(Y_{tr}|X_{tr},\theta)p(\theta) d\theta}

结果:采用p(θ)p(\theta)分布比仅仅采用一个θML\theta_{ML}有着更好地效果

  • 模型融合总是比一个最优模型的效果更好
  • 后验分布里面含有所有从训练数据中学到的相关内容,并且可以模型提取用于计算新的后验分布

Testing阶段:

  • 从training中我们得到了后验分布p(θXtr,Ytr)p(\theta|X_{tr},Y_{tr})
  • 获得了新的的数据点xx
  • 需要计算对于yy的预测

p(yx,Xtr,Ytr)=p(yx,θ)p(θXtr,Ytr)dθp(y|x,X_{tr},Y_{tr}) = \int p(y|x,\theta) p(\theta|X_{tr},Y_{tr})d\theta

重新看一遍

训练阶段:

p(θXtr,Ytr)=p(YtrXtr,θ)p(θ)p(YtrXtr,θ)p(θ)dθp(\theta|X_{tr},Y_{tr}) =\frac{p(Y_{tr}|X_{tr},\theta)p(\theta)}{\color{red}{\int p(Y_{tr}|X_{tr},\theta)p(\theta) d\theta}}

测试阶段:

p(yx,Xtr,Ytr)=p(yx,θ)p(θXtr,Ytr)dθp(y|x,X_{tr},Y_{tr}) =\color{red}{\int p(y|x,\theta) p(\theta|X_{tr},Y_{tr})d\theta }

红色部分的内容通常是难以计算的!

共轭分布

我们说分布p(y)p(y)p(xy)p(x|y)是共轭的当且仅当p(yx)p(y|x)p(y)p(y)是同类的,即后验分布和先验分布同类。

p(y)A(α),p(xy)B(β)p(yx)A(α)p(y)\in \mathcal A(\alpha),\quad p(x|y)\in \mathcal B(\beta)\quad \Rightarrow \quad p(y|x)\in \mathcal A(\alpha^{\prime})

Intuition:

p(yx)=p(xy)p(y)p(xy)p(y)dyp(xy)p(y)p(y|x) = \frac{p(x|y)p(y)}{\int p(x|y)p(y)dy}\propto p(x|y)p(y)

  • 由于任何A\mathcal A中的分布是归一化的,分母是可计算的
  • 我们需要做的就是计算α\alpha^{\prime}

这种情况下贝叶斯推断可以得到闭式解!

常见的共轭分布如下表:

Likelihood p(xy)p(x\mid y) yy Conjugate prior p(y)p(y)
Gaussian μ\mu Gaussian
Gaussian σ2\sigma^{-2} Gamma
Gaussian (μ,σ2)(\mu,\sigma^{-2}) Gaussian-Gamma
Multivariate Gaussian Σ1\Sigma^{-1} Wishart
Bernoulli pp Beta
Multinomial (p1,,pm)(p_1,\ldots,p_m) Dirichlet
Poisson λ\lambda Gamma
Uniform θ\theta Pareto

举个例子:丢硬币

  • 我们有一枚可能是不知道是否均匀的硬币
  • 任务是预测正面朝上的概率θ\theta
  • 数据:X=(x1,,xn)X=(x_1,\ldots,x_n)x{0,1}x\in\{0,1\}

概率模型如下:

p(x,θ)=p(xθ)p(θ)p(x,\theta) = p(x|\theta)p(\theta)

其中对于p(xθ)p(x|\theta)的似然为:

Bern(xθ)=θx(1θ)1xBern(x|\theta) = \theta^x(1-\theta)^{1-x}

但是不知道p(θ)p(\theta)的先验是多少

怎样选择先验概率分布?

  • 正确的定义域:θ[0,1]\theta \in [0,1]
  • 包含先验知识:一枚硬币是均匀的可能性非常大
  • 推断复杂度的考虑:使用共轭先验

Beta分布是满足所有条件的!

Beta(θa,b)=1B(a,b)θa1(1θ)b1Beta(\theta|a,b) = \frac{1}{\mathrm{B}(a,b)}\theta^{a-1}(1-\theta)^{b-1}

同样也适用于大部分不均匀硬币的情况

让我们来检验似然和先验是不是共轭分布:

p(xθ)=θx(1θ)1xp(θ)=1B(a,b)θa1(1θ)b1p(x|\theta)=\theta^x(1-\theta)^{1-x}\qquad p(\theta) = \frac{1}{\mathrm{B}(a,b)}\theta^{a-1}(1-\theta)^{b-1}

方法——检验先验和后验是不是在同样的参数族里面

p(θ)=Cθα(1θ)βp(θx)=Cp(xθ)p(θ)=Cθx(1θ)1x1B(a,b)θa1(1θ)b1=CB(a,b)θx+a1(1θ)bx=Cθα(1θ)β\begin{aligned} p(\theta)&=C\theta^{\alpha}(1-\theta)^{\beta}\\ p(\theta|x)&=C^{\prime}p(x|\theta)p(\theta)\\ &=C^{\prime}\theta^x(1-\theta)^{1-x}\frac{1}{\mathrm{B}(a,b)}\theta^{a-1}(1-\theta)^{b-1}\\ &=\frac{C^{\prime}}{\mathrm{B}(a,b)}\theta^{x+a-1}(1-\theta)^{b-x}\\ &=C^{\prime\prime}\theta^{\alpha^{\prime}}(1-\theta)^{\beta^{\prime}} \end{aligned}

由于先验和后验形式相同,所以确实是共轭的!

现在考虑接收到数据之后的贝叶斯推断:

p(θX)=1Zp(Xθ)p(θ)=1Zp(θ)i=1np(xiθ)=1Z1B(a,b)θa1(1θ)b1i=1nθxi(1θ)1xi=1Zθa+i=1nxi1(1θ)b+ni=1n1=1Zθa1(1θ)b1\begin{aligned} p(\theta|X) &= \frac{1}{Z}p(X|\theta)p(\theta)\\ &= \frac{1}{Z}p(\theta)\prod_{i=1}^{n} p(x_i|\theta) \\ &=\frac{1}{Z} \frac{1}{\mathrm{B}(a,b)}\theta^{a-1}(1-\theta)^{b-1} \prod_{i=1}^{n} \theta^{x_i}(1-\theta)^{1-x_i}\\ &=\frac{1}{Z^{\prime}}\theta^{a+\sum_{i=1}^n x_i -1}(1-\theta)^{b+n-\sum_{i=1}^n-1}\\ &=\frac{1}{Z^{\prime}}\theta^{a^\prime -1}(1-\theta)^{b^\prime -1} \end{aligned}

新的参数为:

a=a+i=1nxib=b+ni=1nxia^\prime = a+\sum_{i=1}^nx_i \qquad b^{\prime}=b+n-\sum_{i=1}^nx_i

那么问题来了,当没有共轭分布的时候我们应该怎么做?

最简单的方法:选择可能性最高的参数

训练阶段:

θMP=argmaxp(θXtr,Ytr)=argmaxp(YtrXtr,θ)p(θ)\theta_{MP}=\arg \max p(\theta|X_{tr},Y_{tr}) = \arg \max p(Y_{tr}|X_{tr},\theta)p(\theta)

测试阶段:

p(yx,Xtr,Ytr)=p(yx,θ)p(θ,Xtr,Ytr)dθp(yx,θMP)p(y|x,X_{tr},Y_{tr}) = \int p(y|x,\theta)p(\theta,X_{tr},Y_{tr})d\theta \approx p(y|x,\theta_{MP})

这种情况下我们并不能计算出正确的后验。

基本介绍

传统处理数据集中缺失值一般有两种方法:

  1. 直接针对缺失值进行建模
    • 对于每个数据集,需要单独建模
  2. 对于缺失值进行填充得到完整数据集,再用常规方法进行分析
    • 删除法,会丢失到一些重要信息,缺失率越高,情况越严重
    • 用均值/中位数/众数填充,没有利用现有的其他信息
    • 基于机器学习的填充方法
      • EM
      • KNN
      • Matrix Factorization

考虑一个这样的数据,时间序列XXT=(t0,,tn1)T=(t_0, \ldots, t_{n-1})的一个观测,X=(xt0,,xti,,xtn1)Rn×dX=\left(x_{t_{0}}, \ldots, x_{t_{i}}, \ldots, x_{t_{n-1}}\right)^{\top} \in \mathbb{R}^{n \times d},例如:

X=[16 none 97 none 7 none 9 none  none 79],T=[0513]X=\left[\begin{array}{cccc}{1} & {6} & {\text { none }} & {9} \\ {7} & {\text { none }} & {7} & {\text { none }} \\ {9} & {\text { none }} & {\text { none }} & {79}\end{array}\right], T=\left[\begin{array}{c}{0} \\ {5} \\ {13}\end{array}\right]

利用mask矩阵MRn×dM\in \mathbb{R}^{n \times d}来表示XX中的值存在与否,如果存在,Mtij=1M^{j}_{t_i}=1否则的话Mtij=0M^{j}_{t_i}=0

总体的基本框架如下,generator从随机的输入中生成时间序列数据,discriminator尝试判别是真的数据还是生成的假数据,通过bp进行优化:

GAN框架

由于最初始的GAN容易导致模型坍塌的问题,采用WGAN(利用Wasserstein距离),他的loss如下:

LG=EzPg[D(G(z))]LD=EzPg[D(G(z))]ExPr[D(x)]\begin{array}{c}{L_{G}=\mathbb{E}_{z \sim P_{g}}[-D(G(z))]} \\ {L_{D}=\mathbb{E}_{z \sim P_{g}}[D(G(z))]-\mathbb{E}_{x \sim P_{r}}[D(x)]}\end{array}

采用基于GRU的GRUI单元作为G和D的基本网络,来缓解时间间隔不同所带来的的问题。可以知道的是,老的观测值所带来的影响随着时间的推移应当更弱,因为他的观测值已经有了一段时间的缺失。

时间衰减(time decay)

采用一个time lag矩阵δRn×d\delta\in \mathbb{R}^{n\times d}来表示当前值和上一个有效值之间的时间间隔。

δtij={titi1,Mti1j==1δti1j+titi1,Mti1j==0&i>00,i==0;δ=[00005555813813]\delta_{t_{i}}^{j}=\left\{\begin{array}{ll}{t_{i}-t_{i-1},} & {M_{t_{i-1}}^{j}==1} \\ {\delta_{t_{i-1}}^{j}+t_{i}-t_{i-1},} & {M_{t_{i-1}}^{j}==0 \& i>0} \\ {0,} & {i==0}\end{array} \quad ; \quad \delta=\left[\begin{array}{cccc}{0} & {0} & {0} & {0} \\ {5} & {5} & {5} & {5} \\ {8} & {13} & {8} & {13}\end{array}\right]\right.

利用一个时间衰减向量β\beta来控制过去观测值的影响,每一个值都应当是在(0,1](0,1]的,并且可以知道的是,δ\delta中的值越大,β\beta中对应的值应当越小,其中WβW_{\beta}更希望是一个完全的矩阵而不是对角阵。

βti=1/emax(0,Wβδti+bβ)\beta_{t_i} = 1/ e^{\max(0,W_{\beta}\delta_{t_i}+b_{\beta})}

GRUI

GRUI的更新过程如下:

hti1=βtihti1μti=σ(Wμ[hti1,xti]+bμ)rti=σ(Wr[hti1,xti]+br)h~ti=tanh(Wh~[rtihti1,xti]+bh~)hti=(1μti)hti1+μtih~ti\begin{aligned}h_{t_{i-1}}^{\prime}&=\beta_{t_{i}} \odot h_{t_{i-1}}\\ \mu_{t_{i}} &= \sigma(W_{\mu}\left[h_{t_{i-1}}^{\prime},x_{t_{i}}\right]+b_{\mu}) \\ r_{t_{i}} &= \sigma(W_{r}\left[h_{t_{i-1}}^{\prime},x_{t_{i}}\right]+b_{r}) \\ \tilde{h}_{t_{i}} &= \tanh(W_{\tilde{h}}\left[r_{t_{i}} \odot h_{t_{i-1}}^{\prime},x_{t_{i}}\right]+b_{\tilde{h}})\\ h_{t_{i}}&=(1-\mu_{t_{i}})\odot h_{t_{i-1}}^{\prime}+\mu_{t_{i}}\odot \tilde{h}_{t_{i}}\end{aligned}

D和G的结构:

D过一个GRUI层,最后一个单元的隐层表示过一个FC(带dropout)

G用一个GRUI层和一个FC,G是自给的网络(self-feed network),当前的输出会作为下一个迭代的输入。最开始的输入是一个随机噪声。假数据的δ\delta的每一行都是常量。

G和D都采用batch normalization。

缺失值填补方法

考虑到xx的缺失,可能G(z)G(z)xx没有缺失的几个值上面都表现的非常好,但是却可能和实际的xx差得很多。

文章中定义了一个两部分组成的loss function来衡量填补的好坏。第一部分叫做masked reconstruction loss,用来衡量和原始不完整的时间序列数据之间的距离远近。第二部分是discriminative loss,让生成的G(z)G(z)尽可能真实。

Masked Reconstruction Loss

只考虑没有缺失值之间的平方误差

Lr(z)=XMG(z)M2L_{r}(z)=\|X \odot M-G(z) \odot M\|_{2}

Discriminative Loss

Ld(z)=D(G(z))L_d(z) = -D(G(z))

Imputation Loss

Limputation(z)=Lr(z)+λLd(z)L_{imputation}(z) = L_{r}(z)+\lambda L_{d}(z)

对于每个原始的时间序列xx,从高斯分布中采样zz,通过一个已经训练好的GG获得G(z)G(z)。之后通过最小化Limputation(z)L_{imputation}(z)来进行训练,收敛之后用G(z)G(z)填充缺失的部分。

ximputed=Mx+(1M)G(z)x_{imputed} = M\odot x+(1-M)\odot G(z)

这一章采用神经网络方法来搭建模型,从而能够解决更为实际的问题。

神经网络单元

一个神经单元可以看做o=f(wx+b)o = f(w*x+b),一个线性的变换再加上一个非线性的激活函数,常见的激活函数如下:

其中ReLU是最为通用的激活函数!

激活函数的通用特征:

  • 非线性
  • 可导(可以存在点不连续,比如Hardtanh和ReLU)
  • 有至少一个敏感的域,输入的变化会改变输出的变化
  • 有至少一个不敏感的域,输入的变化对输出的变化无影响或极其有限
  • 当输入是负无穷的时候有lower bound,当输入是正无穷的时候有upper bound(非必须)

PyTorch中的nn

PyTorch中有一系列构建好的module来帮助构造神经网络,一个module是nn.Module基类派生出来的一个子类。每个Module有一个或多个Parameter对象。一个Module同样可以可以由一个或多个submodules,并且可以同样可以追踪他们的参数。

注意:submodules不能再list或者dict里面。否则的话优化器没有办法定位他们,更新参数。如果要使用submodules的list或者dict,PyTorch提供了nn.ModuleListnn.ModuleDict

直接调用nn.Module实际上等同调用了forward方法,理论上调用forward也可以达到同样的效果,但是实际上不应该这么操作。

现在的training loop长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def training_loop(n_epochs, optimizer, model, loss_fn, t_u_train, t_u_val, t_c_train, t_c_val):
for epoch in range(1, n_epochs + 1):
t_p_train = model(t_u_train)
loss_train = loss_fn(t_p_train, t_c_train)

t_p_val = model(t_u_val)
loss_val = loss_fn(t_p_val, t_c_val)

optimizer.zero_grad()
loss_train.backward()
optimizer.step()

if epoch == 1 or epoch %1000 == 0:
print("Epoch {}, Training loss {}, Validation loss {}".format(epoch, float(loss_train), float(loss_val)))

调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
linear_model = nn.Linear(1,1)
optimizer = optim.SGD(linear_model.parameters, lr=1e-2)

training_loop(
n_epochs = 3000,
optimizer = optimizer,
model = linear_model,
loss_fn = nn.MSELoss(),
t_u_train = t_un_train,
t_u_val = t_un_val,
t_c_train = t_c_train,
t_c_val = t_c_val)

现在考虑一个稍微复杂一点的情况,一个线性模型套一个激活函数再套一个线性模型,PyTorch提供了nn.Sequential容器:

1
2
3
seq_model = nn.Sequential(nn.Linear(1,13),
nn.Tanh(),
nn.Linear(13,1))

可以通过model.parameters()来得到里面的参数:

1
[param.shape for param in seq_model.parameters()]

如果一个模型通过很多子模型构成的话,能够通过名字辨别是非常方便的事情,PyTorch提供了named_parameters方法

1
2
for name, param in seq_model.named_parameters():
print(name,param.shape)

Sequential按模块在里面出现的顺序进行排序,从0开始命名。Sequential同样接受OrderedDict,可以在里面对传入Sequential的每个model进行命名

1
2
3
4
5
6
7
8
from collections import OrderedDict

seq_model = nn.Seqential(OrderedDict([('hidden_linear',nn.Linear(1,8)),
('hidden_activation',nn.Tanh()),
('outpu_linear',nn.Linear(8,1))]))

for name, param in seq_model.named_parameters():
print(name,param.shape)

同样可以把子模块当做属性来对于特定的参数进行访问:

1
seq_model.output_linear.bias

可以定义nn.Module的子类来更大程度上的自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SubclassModel(nn.Module):
def __init__(self):
super().__init__()
self.hidden_linear = nn.Linear(1,11)
self.hidden_activation = nn.Tanh()
self.output_linear = nn.Linear(11,1)

def forward(self, input):
hidden_t = self.hidden_linear(input)
activated_t = self.hidden_activation(hidden_t)
output_t = self.output_linear(activated_t)

return output_t

subclass_model = SubclassModel()

这样极大提高了自定义能力,可以在forward里面做任何你想做的事情,甚至可以写类似于activated_t = self.hidden_activation(hidden_t) if random.random() >0.5 else hidden_t,由于PyTorch采用的是动态的运算图,所以无论random.random()返回的是什么都可以正常运行。

在subclass内部所定义的module会自动的注册,和named_parameters中类似。nn.ModuleListnn.ModuleDict也会自动进行注册。

PyTorch中有functional,它代表输出完全由输入决定,像nn.Tanh这种可以直接写在forward里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SubclassFunctionalModel(nn.Module):
def __init__(self):
super().__init__()

self.hidden_linear = nn.Linear(1,14)

self.output_linear = nn.Linear(14,1)
def forward(self, input):
hidden_t = self.hidden_linear(input)
activated_t = torch.tanh(hidden_t)
output_t = self.output_linear(activated_t)

return output_t
func_model = SubclassFunctionalModel()

在PyTorch1.0中有许多函数被放到了torch命名空间中,更多的函数留在torch.nn.functional里面。

开普勒从数据中得到三定律,同样利用的是现在数据科学的思想,他的步骤如下:

  1. 得到数据
  2. 可视化数据
  3. 选择最简单的可能模型来拟合数据
  4. 将数据分成两部分,一部分用来推导,另一部分用来检验
  5. 从一个奇怪的初始值除法逐渐迭代
  6. 在独立的验证集上检验所得到的模型
  7. 尝试对模型进行解释

今日的学习方法实际上就是自动寻找适合的函数形式来拟合输入输出,流程如下:

输入测试数据->计算输出->计算误差->反向传播->更新权重

问题示例

一个简单的摄氏度和华氏度转换的方法。

定义model和loss函数:

1
2
3
4
5
6
def model(t_u, w, b):
return w * t_u + b

def loss_fn(t_p, t_c):
squared_diffs = (t_p - t_c)**2
return squared_diffs.mean()

正向过程:

1
2
3
4
5
6
w = torch.ones(1)
b = torch.zeros(1)

t_p = model(t_u, w, b)

loss = loss_fn(t_p, t_c)

采用梯度下降进行反向传播,这里采用最简单的方法进行梯度的模拟计算:

1
2
3
4
5
6
7
8
9
delta = 0.1
learning_rate = 1e-2

loss_rate_of_change_w = (loss_fn(model(t_u, w+delta, b), t_c) - (loss_fn(model, t_u, w-delta, b), t_c)) / (2.0*delta)

loss_rate_of_change_b = (loss_fn(model(t_u, w, b+delta), t_c) - (loss_fn(model, t_u, w, b-delta), t_c)) / (2.0*delta)

w -= learning_rate * loss_rate_of_change_w
b -= learning_rate * loss_rate_of_change_b

上面这种方法会存在误差,可以考虑采用链式法则进行导数的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def loss_fn(t_p, t_c):
squared_diffs = (t_p - t_c)**2
return squared_diffs.mean()

def dloss_fn(t_p, t_c):
dsq_diffs = 2 * (t_p - t_c)
return dsq_diffs

def model(t_u, w, b):
return w * t_u + b

def dmodel_dw(t_u, w, b):
return t_u

def dmodel_db(t_u, w, b):
return 1.0

def grad_fn(t_u, t_c, t_p, w, b):
dloss_dw = dloss_fn(t_p, t_c) * dmodel_dw(t_u, w, b)
dloss_db = dloss_fn(t_p, t_c) * dmodel_db(t_u, w, b)
return torch.stack([dloss_dw.mean(), dloss_db.mean()]) # 利用stack合成一个tensor

对于一个训练轮次可以写成下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
for epoch in range(1, n_epochs+1):
w, b = params

t_p = model(t_u, w, b)
loss = loss_fn(t_p, t_c)
grad = grad_fn(t_u, t_c, t_p, w, b)

params = parmas - learning_rate * grad

print("Epoch %d, Loss %f" % (epoch, float(loss)))
return params

对于不同的参数,可能得到的梯度大小会很不一样,一般将所有的输入做一个标准化的操作,从而能够使得训练更有效的收敛。

Autograd

autograd可以自动的根据运算求出导数,而不需要手动的对复杂的函数进行计算,考虑用autograd重写之前的内容:

1
2
3
4
5
6
7
8
def model(t_u, w, b):
return w * t_u + b

def loss_fn(t_p, t_c):
squared_diffs = (t_p - t_c)**2
return squared_diffs.mean()

params = torch.tensor([1.0, 0.0], requires_grad = True)

requires_grad的效果是让pytorch在运算过程中对他的值进行追踪,每个参数都有.grad对象,正常情况下值为None

1
2
loss = loss_fn(model(t_u, *params), t_c) # 加*相当于对参数进行解包,分别作为w,b传入
loss.backward()

通过backward()反传之后,params.grad不再是None

多次运算,params上的梯度会被叠加,为了防止这样的事情出现,需要将梯度清零:

1
2
if params.grad is not None:
params.grad.zero_()

现在训练过程长这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
for epoch in range(1, n_epochs + 1):
if params.grad is not None:
params.grad.zero_()

t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()

params = (params - learning_rate * params.grad).detach().requires_grad_()

if epoch % 500 == 0:
print('Epoch %d, Loss %f' % (epoch, float(loss)))
return params

detach将旧版本的参数从运算图中分离,requires_grad_使得参数可以被追踪导数。调用方法如下:

1
2
3
4
5
6
training_loop(
n_epochs = 5000,
learning_rate = 1e-2,
params = torch.tensor([1.0,0.0], requires_grad = True),
t_u = t_un,
t_c = t_c)

Optimizer

可以通过下面的方法列出所有的优化器:

1
2
3
import torch.optim as optim

dir(optim)

每个优化器在构造的时候都针对一系列的参数(requires_grad = True),每个参数都被存在优化器内部,使得可以通过访问grad来对他们进行更新。

每个优化器都有两个方法:zero_gradstep,前者将所有在构建优化器时候传入的参数的grad全部设置成0,后者通过优化器自己的方法利用梯度对参数进行更新。

1
2
3
4
5
6
7
8
9
10
params = torch.tensor([1.0, 0.0], requires_grad = True)
learning_rate = 1e-5
optimizer = optim.SGD([params], lr = learning_rate)

t_p = model(t_un, * params)
loss = loss_fn(t_p, t_c)
# 正常的流程
optimizer.zero_grad()
loss.backward()
optimizer.step()

更改之后的训练流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
def training_loop(n_epochs, optimizer, params, t_u, t_c):
for epoch in range(1, nepochs + 1):
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)

optimizer.zero_grad()
loss.backward()
optimizer.step()

if epoch%500 == 0:
print('Epoch %d, Loss %f' % (epoch, float(loss)))

return params

训练集,验证集和过拟合

规则一:如果训练loss不下降,那么可能是模型太简单,或者是输入的信息不能很好地解释输出

规则二:如果验证集loss偏离,说明过拟合

缓解过拟合方法:

  1. 添加正则项
  2. 给输入加噪声生成新的数据
  3. 采用更简单的模型

可以考虑利用随机排序的下标来获得shuffle后的训练集和验证集:

1
2
3
4
5
6
7
n_samples = t_u.shape[0]
n_val = int(0.2*n_sample)

shuffled_indices = torch.randperm(n_samples)

train_indices = shuffled_indices[:-n_val]
val_indices = shuffled_indices[-n_val:]

由于并不会考虑在验证集的loss上反向传播,为验证集构造运算图是非常浪费内存和时间的事情,可以考虑利用torch.no_grad来提升效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
def training_loop(n_epochs, optimizer, params, train_t_u, val_t_u, train_t_c, val_t_c):
for epoch in range(1, n_epochs + 1):
train_t_p = model(train_t_u, *params)
train_loss = loss_fn(train_t_p, train_t_c)

with torch.no_grad():
val_t_p = model(val_t_u, *params)
val_loss = loss_fn(val_t_p, val_t_c)
assert val_loss.requires_grad == False # 确认所有参数的requires_grad是False

optimizer.zero_grad()
train_loss.backward()
optimizer.step()

或者可以使用set_grad_enabled来条件的启用反向传播

1
2
3
4
5
def calc_forward(t_u, t_c, is_train):
with torch.set_grad_enabled(is_train):
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
return loss