笔试部分
还是有一些难度的,没刷过一段时间的题目还可能应付不了这一个小时两道题的强度,记录一下笔试内容。
第一题
Question
有一叠扑克牌,每张牌介于1和10之间
有四种出牌方法:
- 单出1张
- 出2张对子
- 出五张顺子,如12345
- 出三连对子,如112233
给10个数,表示1-10每种牌有几张,问最少要多少次能出完
分析
这个题目如果需要找特定的规则去判断,显然是可以的,但是条件有点多。所以就不考虑这种方式,干脆根据题目中的几种情形分别去回溯,然后对回溯的结果去比较,选择最小的一种方式来打牌。
能出三个对子,显然需要要求,i,i+1,i+2的数目都大于2。同理,要出顺子也要满足一定的条件,我们只需要对该条件进行分别进行判断即可。
大致整理一下代码的核心思路,从小往大出牌,研究每一个数字可以如何出完。如果不能打连对或者连子的时候,出牌的次数是一定的,如果可以打的时候,计算打连对最少需要出的次数,打连子最少出的次数,然后打对子最少需要出的次数,然后最终取最小,就是出完当前数字的最少次数,然后依次计算每个数字出完的最小次数。
这里我只给出核心代码,也并没有处理输入输出的情况。大家自行处理。
Answer
class Solution1:
def leastTimes(self,nums,begin=0):
res=sum(nums[begin:]) # 表示剩余牌的总数
if not res:return 0
if nums[begin]>0:
if begin+2<10 and nums[begin]>1 and nums[begin+1]>1 and nums[begin+2]>1:
for i in range(begin,begin+3): # 打了连对,状态改变
nums[i]-=2
res=min(1+self.leastTimes(nums,begin),res)
for i in range(begin,begin+3): # 回溯,恢复状态
nums[i]+=2
if begin+4<10 and nums[begin+1] and nums[begin+2] and nums[begin+3] and nums[begin+4]:
for i in range(begin, begin + 5): # 打了连子,状态改变
nums[i] -= 1
res=min(1+self.leastTimes(nums,begin),res)
for i in range(begin, begin + 5): # 回溯,恢复状态
nums[i] += 1
if nums[begin]>1:
nums[begin] -= 2 # 出对子,因为出对子之后,可能当前的牌还有剩余,所以继续从当前牌型去搜索。
return min(1+self.leastTimes(nums,begin),res)
else : # 只有一张,只能考虑单张
return min(res,1+self.leastTimes(nums,begin+1))
else: # 表示没有当前牌型,直接打下一张牌
return self.leastTimes(nums,begin+1)
第二题
Question
首先定义上升字符串,\(s[i] \ge s[i-1]\),比如aaa,abc是,acb不是
给n个上升字符串,选择任意个拼起来,问能拼出来的最长上升字符串长度
分析
我们定义一个上升字符串为\(S_{ij}\),表示该字符串的首字符是 \(i\) ,尾字符是 \(j\),则\(dp_j=max(dp_{j−1},dp_i+S_{ij}.size())\quad i≤j\)。解释一下这个公式,表示的是以字符\(j\)结尾的上升字符串最长的长度是以字符\(j-1\)结尾的上升字符串的最长长度,和以字符\(i\)结尾加上字符串从\(S_{ij}\)的和\(S_{hh}\)长度更新 \(dp_{*h}\) 的结果,我们就需要把\(S_{hh}\)放在\(S_{*h}\)之后处理。
也就是说,我们根据最后一个字符排序,如果最后一个字符相同,我们则需要把第一个字符越大的越往后边放。而且排序的过程中,因为最后一个字符只有26种情况,这里我们可以考虑使用基数排序或者桶排序,排序我这里直接使用的是内建的排序,如果大家需要优化可以采用其他的。具体优化细节不谈。
Answer
str_list = []
for _ in range(int(input())):
str_list.append(input().rstrip())
str_list = sorted(str_list, key=lambda x: (x[-1], x[0]))
dp = [0] * 27
for i in range(len(str_list)):
for j in range(26, 0, -1):
tmp = len(str_list[i])
st = str_list[i][0]
end = str_list[i][-1]
if ord(end) - ord('a') + 1 > j:
continue
dp[j] = max(dp[j], dp[ord(st) - ord('a') + 1] + tmp)
print(dp[26])
第一轮面试
这个是个预面试,就完全没有准备的接到了面试,除去简历和项目之外的一些知识点总结一下:
L1 loss与L2 loss的区别和联系
L1 Loss
使用L1损失函数也被叫做最小化绝对误差(Least Abosulote Error)。这个名称非常的形象。LAE就是最小化真实值:
这里的\(D_{L1}\)其实就是平均绝对误差(MAE)
使用L1损失函数也就是\(\min D_{L1}\)
L2 Loss
使用L2损失函数也被叫做最小化平方误差(Least Square Error)。LSE就是最小化真实值
这里的\(D_{L2}\)其实就是平均绝对误差
使用的L2损失函数也就是\(\min D_{L2}\)
L1 | L2 |
---|---|
稳健性高 | 稳健性一般 |
没有稳定的解 | 有稳定的解 |
可能会得到多组解 | 只有一个解 |
稳健性
L1损失函数稳健性强是它最大的优点。面对误差较大的观测,L1损失函数不容易受到它的影响。这是因为L1损失函数增加的只是一个误差,而L2损失函数增加的是误差的平方。当误差较大时,使用L2损失函数,我们需要更大程度的调整模型以适应这个观测,所以L2损失函数没有L1损失函数那么稳定。
那么,当我们认为模型中可能存在异常值时,使用L1损失函数可能会更好;但是,当我们需要把误差较大的观测也纳入模型中时,使用L2损失函数更好一些。
解的稳定性
首先,从求解效率上来说,L2损失函数处处可导,而L1损失函数在零点位置是不可导的,这就使得使用L2损失函数求解可以得到一个解析解,而L1损失函数则没有;
其次,当数据有一个微小的变化时,L1损失函数的变化更大,其解更加的不稳定。
Pytorch中DataLoader Dataset Sampler的作用和关系
简单来说,Sampler定义的是数据读取的方式,可以使随机读取,可以是顺序读取,而Dataset定义的是某一批次的数据,提供数据内容。而DataLoader则是利用Sampler以及Dataset对网络提供数据的另外封装的接口。
比如Sampler分为:
- SequentialSampler
- RandomSampler
- WeightedSampler
- SubsetRandomSampler
代码如下:
class Sampler(object):
r"""Base class for all Samplers.
Every Sampler subclass has to provide an :meth:`__iter__` method, providing a
way to iterate over indices of dataset elements, and a :meth:`__len__` method
that returns the length of the returned iterators.
.. note:: The :meth:`__len__` method isn't strictly required by
:class:`~torch.utils.data.DataLoader`, but is expected in any
calculation involving the length of a :class:`~torch.utils.data.DataLoader`.
"""
def __init__(self, data_source):
pass
def __iter__(self):
raise NotImplementedError
def __len__(self):
return len(self.data_source)
而Dataset里面的结构为:
class Dataset(object):
def __init__(self):
...
def __getitem__(self, index):
return ...
def __len__(self):
return ...
得到什么样的数据,数据返回什么样的长度等等。
比如DataLoader的结构为:
class DataLoader(object):
...
def __next__(self):
if self.num_workers == 0:
indices = next(self.sample_iter)
batch = self.collate_fn([self.dataset[i] for i in indices]) # this line
if self.pin_memory:
batch = _utils.pin_memory.pin_memory_batch(batch)
return batch
collate_fn
的作用就是将一个batch的数据进行合并操作。默认的collate_fn
是将img和label分别合并成imgs和labels,所以如果你的__getitem__
方法只是返回 img, label
,那么你可以使用默认的collate_fn
方法,但是如果你每次读取的数据有img, box, label
等等,那么你就需要自定义collate_fn
来将对应的数据合并成一个batch数据,这样方便后续的训练步骤。
具体可以查看参考文献里面的关系。
BatchNormalization的原理以及参数作用
因此我们引入了这个可学习重构参数\(\gamma,\beta\),让我们的网络可以学习恢复出原始网络所要学习的特征分布。
这个有点复杂了,以后再补充,可以查看参考文献的细节。
知识蒸馏相关内容
知识蒸馏,可以将一个网络的知识转移到另一个网络,两个网络可以是同构或者异构。做法是先训练一个teacher网络,然后使用这个teacher网络的输出和数据的真实标签去训练student网络。知识蒸馏,可以用来将网络从大网络转化成一个小网络,并保留接近于大网络的性能;也可以将多个网络的学到的知识转移到一个网络中,使得单个网络的性能接近emsemble的结果。
公式如下:
T参数是什么?有什么作用?**
T参数为了对应蒸馏的概念,在论文中叫的是Temperature,也就是蒸馏的温度。T越高对应的分布概率越平缓,为什么要使得分布概率变平缓?举一个例子,假设你是每次都是进行负重登山,虽然过程很辛苦,但是当有一天你取下负重,正常的登山的时候,你就会变得非常轻松,可以比别人登得高登得远。
同样的,在这篇文章里面的T就是这个负重包,我们知道对于一个复杂网络来说往往能够得到很好的分类效果,错误的概率比正确的概率会小很多很多,但是对于一个小网络来说它是无法学成这个效果的。我们为了去帮助小网络进行学习,就在小网络的softmax加一个T参数,加上这个T参数以后错误分类再经过softmax以后输出会变大(softmax中指数函数的单增特性,这里不做具体解释),同样的正确分类会变小。这就人为的加大了训练的难度,一旦将T重新设置为1,分类结果会非常的接近于大网络的分类效果。
soft target(“软目标”)是什么?
soft就是对应的带有T的目标,是要尽量的接近于大网络加入T后的分布概率。
hard target(“硬目标”)是什么?
hard就是正常网络训练的目标,是要尽量的完成正确的分类。
两个目标函数究竟是什么?
两个目标函数也就是对应的上面的soft target和hard target。这个体现在Student Network会有两个loss,分别对应上面两个问题求得的交叉熵,作为小网络训练的loss function。
具体蒸馏是如何训练的?
Teacher: 对softmax(T=20)的输出与原始label求loss。
Student:(1)对softmax(T=20)的输出与Teacher的softmax(T=20)的输出求loss1。(2)对softmax(T=1)的输出与原始label求loss2。(3)loss = loss1+loss2
第二轮面试
也是突然打电话过来,突然的面试,又打了一个措手不及。感觉这面试体验有点糟糕。
面试内容和一面类似,这里就补充一下上面没有出现过的一些问题。
可导、可微和连续
对于一元函数有,可微<=>可导=>连续=>可积
对于多元函数,不存在可导的概念,只有偏导数存在。函数在某处可微等价于在该处沿所有方向的方向导数存在,仅仅保证偏导数存在不一定可微,因此有:可微=>偏导数存在=>连续=>可积。
可导与连续的关系:可导必连续,连续不一定可导;
可微与连续的关系:可微与可导是一样的;
可积与连续的关系:可积不一定连续,连续必定可积;
可导与可积的关系:可导一般可积,可积推不出一定可导;
BN 所在网络位置
增加了 Batch Normalization 的 DNN,其训练步骤如下:
- 增加 BN 结构:对 DNN 中每一个 Activation,在它们前面放置一个 BN Layer(Batch Normalization Layer)。相当于以前的将\(Wu+b\)输入 Activation Function,现在将BN( \(Wu+b\))输入 Activation Function。至于为什么是在 Activation Function 前放置,而非整个 Hidden Layer 前放置,我们下面会解释
- 求解参数:利用 BP 求解 DNN 中的参数
可能是因为我笔试做的太烂又加了两道算法题,两个算法题都是手撕代码在线编程,没有编译器只有IDE,做的对不对全靠面试官:
翻转字符串
比较简单,就不写了。因为用的python你懂得。
数组内查找两数之和
也还OK,注意边界条件。
总结
阿里的面试整体很看重你的算法能力和基础能力。可能也是大佬比较多。但是感觉面试体验很差,面试官很着急,不是一个和你探讨问题的过程而是一个快速找到你的认知上下限的过程,并且每次面试没有预约直接打电话随时随地进行面试感觉有一些不礼貌。也不知道是阿里的整体面试风格还是面试官个人风格,暂且不论了。
参考
L1和L2损失函数(L1 and L2 loss function)及python实现
一文弄懂Pytorch的DataLoader, DataSet, Sampler之间的关系
知识蒸馏(Knowledge Distillation)简述(一)
知识蒸馏(Distillation)相关论文阅读(1)——Distilling the Knowledge in a Neural Network(以及代码复现)