秋招基本也结束了,这里对整个过程进行复盘和总结,也是提前备战明年的春招。

这次秋招前期一直0offer,直到11月多才逐渐有差不多几家,最后开出来了一些,最终还是决定去钱多的地方,毕竟现在工作都不稳,钱多才是王道。

面试前的内容

算法 vs 开发

对于算法和开发的选择,我还是选择了算法,他们都说算法要有顶会,但是其实这个因素影响没有那么大的,我觉得不一定需要顶会,但是论文还是要有的,毕竟算法岗。没有顶会的情况下最重要的就是实习了,实习一定要把握住,这个可比顶会好拿到多了。我看往前几届的师兄师姐都去的开发岗,基本都是Java,但是这块我又不擅长,突击几个月还不一定有本科生学的好,干脆all in 算法了。毕竟算法还是比开发高很多的,还是可以冲下的。

另外一个选择的区别就是个人选择和未来规划,如果本科双非,同时没有科研论文,还没有实习,而且不打算后面卷,而是打算过几年回老家附近的地方,或者直接一步到位去二线城市,肯定直接Java,毕竟选择面非常广,Java的岗位数量遥遥领先,而且去国企银行运营商都好找工作。

如果还更有些追求,打算去大厂干几年,算法还是更好的,只是本科双非其实不算主要的问题,前提是有比较好的论文或者顶会,再想办法弄个实习,这样基本还是能找的,但要是都没有,那就不太容易了。

对于算法的准备,主要从以下几个方面展开,大致有力扣,简历,实习,秋招等。

力扣

力扣这是最基本的准备,毕竟除了人才计划,其他无论算法开发岗位都有力扣的考察,所以这个是一定要准备的。力扣的内容基本刷几遍hot100就差不多了,基本面试的手撕都是hot100这个难度,基本从前一年的12月就可以开始每天一道题了,开始的时候会很慢,后面慢慢熟练了就快了。之所以要开始这么早是因为实习是第二年三月就开始了,提前三个月准备刷题还是有必要的。很多人刷300题,基本肯定是够用了,有的人甚至刷了600题,其实不是特别有必要,力扣题主要起到的是一个门槛的作用,笔试不过直接pass,过的情况下主要看面试了。

简历

简历是非常重要的,决定了简历筛选后面能不能进笔试和面试,我简历凑合吧,投了90家,进面大概30家,正常水平吧。简历一般是先教育经历,之后最好是要有实习经历,然后是论文和科研部分,后面是项目可以放几个,后面是比赛获奖那些,主体就这些吧,具体根据不同人,优势不同,做的好的放前面一些。根据篇幅,后面可以加一些个人技能,比如编程语言和计算机工具比如Git、Linux什么的掌握情况,最后篇幅不够可以放自我评价,这个不是必须,可有可无。

对于简历的排版来说,也可直接按照不同模块的顺序,也有按照一个一个项目,然后每个项目后面跟着其中的产出论文成果以及技术栈,不同的都是可以的,根据自己实际情况来,总之自己最突出的地方要尽可能的放到比较显眼的前面。

对于简历是一页还是两页,我看不同HR喜好并不同,有的认为一页简单明了,有的觉得一页太单薄。总之校招一般不超过两页(博士除外),实际来看如果确实有内容建议充实两页,排版不要过于空旷,适当紧凑一些。

面试内容

一般一面或者二面技术面,自我介绍之后,如果有比较有含金量的实习或者论文会先讲这部分,之后会问项目之类的,之后有的会问一些八股,这个八股有的是算法八股比如Transformer、CNN、RNN卷积等什么的,这块需要背下。有的会问到计算机基础比如进程线程还有Python基础比如装饰器,线程锁啥的,这块只是有时候会问到。

然后就是面试中的手撕了,这个不是必须,有的面试会有手撕,这个因岗位和公司不同,手撕大部分是力扣,当然也有例外,比如淘天,投实习的时候手撕是手写多头注意力,交叉熵等题目,还有k-means的,这个不提前准备的话想快速写出来也不太容易。比如华子正式面试二面就出了个数字图像处理的手撕,很难,就蚌埠住了,这种题不多见。

技术面之后一般主管面,这块可能的问题很多,从个人信息到家里情况,到抗压测试,了解新信息的途径,其他还有比如人工智能对各行各业的影响,大模型在不同行业的应用,甚至包括盈利模式啥的等产品问题,不同的侧重点也不同,需要随机应变。这个部分不可小视,相关问题要整理下,大致想想如何回答。有的公司比如华子是技术面了可以捞,但是主管面挂了就不行。

实习

实习的重要程度可以说是第一了,毕竟一般如果组内条件不具备的话,是发不出来顶会的,尤其对于硕士,所以实习就成为了性价比最高而且最可以获取到的了。

准备实习第一步是要提前刷力扣题,然后准备简历润色,之后到三月下实习基本就开始了,四五月是最主要的时间,六月基本就不多了,建议尽早投递,一般越早越容易拿到Offer,前期看哪些先开,最开始的可以不投特别大的厂,因为没有相关经验,如果面的很差的话确实会影响面评,影响后面,不过大部分好像影响不大的,可以先从小厂开始,虽然很多时候大厂先开。在面试的过程中不断复盘,总结经验,不断提高,投递尽量不要拖太后,后面很多没有hc,即使准备的好也没用了。

我前面投递实习的时候技术面其实不会特别难,不过也有例外,比如淘天的,还有腾讯AILab这种,不过多面还是有好处的,对于秋招面试也是积累经验的过程。

如果实在没有实习经历,也是可以把一些横向的项目加进去,包括校企合作,实践活动,这种有的也是算实习的。

实习看能不能转正,如果能转正就要好好准备,不能的话就趁早想想后路,实习的经历如何整理成体系,面试的时候问答到相关的问题如何回答,把工作点整理好,面试的时候就不慌。

除此之外,如果没有特别好的成果的话实习投递也有可能不容易找到,但是这也是一个打怪升级的过程,有实习还是远远大于没有实习的,没有大厂就先去小厂,后面有机会再去大厂,先找一个能去的。

秋招

如果有实习的话一般八九月就要回去准备秋招了,除非对自己非常自信能实习转正。对于有实习的有经历,但是秋招的准备会少一些时间,因为实习很多时候也挺忙的,个别实习好像能直接在公司刷力扣,不过这种极少,看部门和主管。没有实习的人秋招七八月的时候有比较完整的时间准备秋招的内容比如力扣八股,把项目好好复盘。

建议还是尽早投递,确实是越早越容易,所以早点开始准备秋招的时候就可以占有不少优势,前后期难度确实不同,基本上八月份面试的都容易进。然后整理一个表格,不同公司的啥时候开始投递,有些相关的群要关注下,有的学长学姐会进行整理,自己这个表格记录好公司、投递时间、岗位、base地,笔试面试情况和时间啥的,方便自己查看。

算法方向

对于算法方向,目前岗位很多是算法工程师或者AI工程师,这块需要的技能很多时候不是只限一个方面的,很多岗位问的时候是既有传统算法也有大模型,所以技能肯定是越多越好,绝大部分算法都不是研究型的算法,而是业务型的算法,这样的算法主要是以解决实际问题来的,无论传统算法还是深度学习或者大模型,根据实际问题选择,所以都会最好。当然也有不少是直接的大模型算法工程师,虽然这种岗问的时候也还是有一般的算法。具体来说,主要从以下几个方向展开。

视觉方向

这个方向也是前面做的人最多的方向,虽然很多人现在做的还是视觉的项目或者科研,但是确实不建议用这个来找工作,除非你能发CCF-C及以上的论文吧,虽然C也很勉强。现在视觉的岗位太少了,所以必须是做的比较深,有一定的研究才行,不然相关的算法都很成熟了,不太好用来找工作。

这个方向现在不适合单独作为一个方向找工作,但是也是算法工程师基本必不可少的一个基础,还是要会一些相关内容更好的。

以下是两个半可以做的方向,说是两个半是因为第三个方向部署推理这块的hc远远不如前面两个的。

大模型

这个方向是目前算法最好找工作的方向了,大语言模型和AIGC这块的需求很多。很多人都说自己没做过,可问题是22年11月底ChatGPT出来之前,有几个人是做大模型的?现在做大模型的人99.9%的都是大模型出来之后开始学的,这块的上手门槛没有那么高的。很多人准备一两个月做一个大致像样的项目,用来参加比如书生浦语浦源大模型挑战赛,或者阿里这种的大模型比赛,差不多获奖就可以拿来简历用了,只要被问到的时候能讲清楚就还是可以的。现在各行各业都在用大模型做一遍,仍然是个可以做的风口,虽然风口过去并不知道能做成啥样,但是这几年自己能赚到就可以了。

学习这个并不需要一上来直接看论文,看看相关的项目,和一些相对比较容易的开源课程,先上手了解整个体系,然后尝试做个玩具微调下,之后再去认真研究,是个比较好的学习方法。

搜广推

这个方向作为互联网的基础,有非常稳定的基础,但是对于应届生来说学校期间往往不做这块的内容,不过要是突击两个月还是可以做下的,不算特别热,但是hc还可以,互联网都招。

部署推理

这个方向涉及模型的量化、剪枝、蒸馏、推理、部署、推理框架、推理引擎,还要AI Infra这些,这些方向其实还是可以做的,尤其现在大模型落地对这块的需求还是不少的,即使不是大模型的部署,小模型的部署也是有需求的。不过这块就要深挖下了,很多时候涉及C++,难度就上来了,不过这块的hc数就远比不上前面了,而且相对不太好转方向,有的开的价还可以,有的不如算法,但是还是比开发高的,也算是一个可以尝试的方向。tensorrt、ncnn、tvm、onnnx,还有vllm、turbomind、triton这些都是可以学习的。

这个方向因为hc极少,只攻这一个方向还是不好找工作,但是问题在于,算法工程师的技能点之一是CPP和量化部署这块,包括AI研发岗,我秋招面的很多家都会问到这块学的如何,也问相关的细节,所以这块如果会一些也是有好处的。

面试内容总结

简历学习内容

大模型

实习相关

实验评价指标

  • 正则匹配如何做的
  • 类别非常不均的情况下 acc 指标是否足够,不足使用了什么
  • 语义理解的评价指标
  • 具体指标
  • 为什么这样用
  • 这一指标达到多少,什么意义
  • badcase 有多少,是什么原因,如何处理的

如何根据实验结果调整 prompt

数据构造

  • 数据量
  • 构造方法
  • 实际上下文长度是多少
  • 数据截断是什么问题

数据如何标注的

  • 真值从哪里来
  • 标注的数据有什么问题

采用大模型而不是之前的机器学习方法的意义、优点

改进和上线所需措施

  • 模型是否满足上线需要
  • 如果不满足是因为什么,效果或速度还是其他
  • 上线需要解决其他什么问题,后续需要做什么

相关理论

  • 主流的大模型参数、量级、结构、最大上下文长度,如何选取
    • Llama 系列
    • 千问
    • 盘古智子
    • chatglm
    • internlm
    • 其他
    • openai o1
  • 大模型评测方法
  • CoT 和实习中的应用
  • 微调方法
    • LoRA、QLoRA 等多种方法
      • 多头 LoRA
      • LoRA
      • QLoRA
    • 训练了哪些层
    • 设置了哪些超参数,如 ralpha
    • 如何根据实际情况调整这些参数
  • 分布式训练方法
    • deepspeed
    • zero

RAG 相关

  • 原理
  • 解决什么问题,为什么用 RAG
  • RAG 的向量表征方法都有哪些,怎么做的

视觉

目标检测

  • 模型
    • YOLOv1~v11
    • 各代演进和区别
    • 总体发展理论
    • DETR 等无 nms 的模型
  • 模型架构
    • 主干网络的变化和区别
    • 金字塔融合方式的多种
    • 中间模块和处理方法
  • 图像增强
    • 一般的几种
    • mosaic 等多种方法
  • 量化
    • 量化方法
    • 参数量
    • 速度精度
  • 剪枝
    • 方法
    • 对网络什么部分进行剪枝
    • 参数量变化
    • 速度变化
  • CPP 部分主要包括哪些
    • 具体这部分如何用的

人脸识别

  • 人脸识别和检测部分
  • 构建索引和检索理论
  • 向量表征
  • 相似度检索
  • 活体识别

算法八股

理论

  • Transformer 模型讲解
  • LayerNorm 等多种 norm 方式如 BatchNorm
  • Encoder 和 Decoder 相关
  • RNN、LSTM、Transformer 变化
  • 大模型中激活函数等多种常用的结构
    • GQA
    • MQA
    • RMSNorm
    • SwiGLU
    • RoPE
  • 激活函数
    • ReLU
    • Leaky ReLU
    • GELU
    • tanh
    • sigmoid
    • swish
    • ELU
  • 特征工程包括什么,如何做
    • 常见的数据预处理方法有哪些
  • 分布式数据处理原理(如 Hadoop)
  • 计算机理论相关(如堆和栈的区别)

手撕

  • Transformer
  • 注意力
  • LayerNorm
  • KMeans
  • NMS
  • IOU 计算

最后补个学习的内容:算法工程师面试常考手撕题

引用链接https://mp.weixin.qq.com/s/TAFvUlqdyqP-W6C10F1Hzw

  • 算法工程师面试常考手撕题
    • 注意力(Attention)篇
      • 手撕单头注意力机制(ScaledDotProductAttention)函数
      • 手撕多头注意力(MultiHeadAttention)
      • 手撕自注意力机制函数(SelfAttention)
    • 基础机器学习算法篇
      • 手撕 k-means 算法
    • 手撕 Layer Normalization 算法
    • 手撕 Batch Normalization 算法
    • 解码算法篇
      • 手撕 贪心搜索 (greedy search)
    • 神经网络篇
      • 手撕 卷积神经网络(CNN)法
      • 手撕 二维卷积 算法
    • 位置编码篇
      • 手撕 绝对位置编码 算法
      • 手撕 可学习位置编码 算法
      • 手撕 相对位置编码 算法
      • 手撕 rope 算法
    • 面试题汇总
    • 致谢

注意力(Attention)篇

手撕单头注意力机制(ScaledDotProductAttention)函数

输入是query和 key-value,注意力机制首先计算query与每个key的关联性(compatibility),每个关联性作为每个value的权重(weight),各个权重与value的乘积相加得到输出。
image.jpg

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
class ScaledDotProductAttention(nn.Module):
""" Scaled Dot-Product Attention """


def __init__(self, scale):
super().__init__()


self.scale = scale
self.softmax = nn.Softmax(dim=2)


def forward(self, q, k, v, mask=None):
u = torch.bmm(q, k.transpose(1, 2)) # 1.Matmul
u = u / self.scale # 2.Scale


if mask is not None:
u = u.masked_fill(mask, -np.inf) # 3.Mask


attn = self.softmax(u) # 4.Softmax
output = torch.bmm(attn, v) # 5.Output


return attn, output




if __name__ == "__main__":
n_q, n_k, n_v = 2, 4, 4
d_q, d_k, d_v = 128, 128, 64


q = torch.randn(batch, n_q, d_q)
k = torch.randn(batch, n_k, d_k)
v = torch.randn(batch, n_v, d_v)
mask = torch.zeros(batch, n_q, n_k).bool()


attention = ScaledDotProductAttention(scale=np.power(d_k, 0.5))
attn, output = attention(q, k, v, mask=mask)


print(attn)
print(output)

手撕多头注意力(MultiHeadAttention)

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
class MultiHeadAttention(nn.Module):
""" Multi-Head Attention """


def __init__(self, n_head, d_k_, d_v_, d_k, d_v, d_o):
super().__init__()


self.n_head = n_head
self.d_k = d_k
self.d_v = d_v


self.fc_q = nn.Linear(d_k_, n_head * d_k)
self.fc_k = nn.Linear(d_k_, n_head * d_k)
self.fc_v = nn.Linear(d_v_, n_head * d_v)


self.attention = ScaledDotProductAttention(scale=np.power(d_k, 0.5))


self.fc_o = nn.Linear(n_head * d_v, d_o)


def forward(self, q, k, v, mask=None):


n_head, d_q, d_k, d_v = self.n_head, self.d_k, self.d_k, self.d_v


batch, n_q, d_q_ = q.size()
batch, n_k, d_k_ = k.size()
batch, n_v, d_v_ = v.size()


q = self.fc_q(q) # 1.单头变多头
k = self.fc_k(k)
v = self.fc_v(v)
q = q.view(batch, n_q, n_head, d_q).permute(2, 0, 1, 3).contiguous().view(-1, n_q, d_q)
k = k.view(batch, n_k, n_head, d_k).permute(2, 0, 1, 3).contiguous().view(-1, n_k, d_k)
v = v.view(batch, n_v, n_head, d_v).permute(2, 0, 1, 3).contiguous().view(-1, n_v, d_v)


if mask is not None:
mask = mask.repeat(n_head, 1, 1)
attn, output = self.attention(q, k, v, mask=mask) # 2.当成单头注意力求输出


output = output.view(n_head, batch, n_q, d_v).permute(1, 2, 0, 3).contiguous().view(batch, n_q, -1) # 3.Concat
output = self.fc_o(output) # 4.仿射变换得到最终输出


return attn, output




if __name__ == "__main__":
n_q, n_k, n_v = 2, 4, 4
d_q_, d_k_, d_v_ = 128, 128, 64


q = torch.randn(batch, n_q, d_q_)
k = torch.randn(batch, n_k, d_k_)
v = torch.randn(batch, n_v, d_v_)
mask = torch.zeros(batch, n_q, n_k).bool()


mha = MultiHeadAttention(n_head=8, d_k_=128, d_v_=64, d_k=256, d_v=128, d_o=128)
attn, output = mha(q, k, v, mask=mask)


print(attn.size())
print(output.size())

手撕自注意力机制函数(SelfAttention)

Self-Attention。和Attention类似,他们都是一种注意力机制。不同的是Attention是source对target,输入的source和输出的target内容不同。例如英译中,输入英文,输出中文。而Self-Attention是source对source,是source内部元素之间或者target内部元素之间发生的Attention机制,也可以理解为Target=Source这种特殊情况下的注意力机制。

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
class SelfAttention(nn.Module):
""" Self-Attention """


def __init__(self, n_head, d_k, d_v, d_x, d_o):
self.wq = nn.Parameter(torch.Tensor(d_x, d_k))
self.wk = nn.Parameter(torch.Tensor(d_x, d_k))
self.wv = nn.Parameter(torch.Tensor(d_x, d_v))


self.mha = MultiHeadAttention(n_head=n_head, d_k_=d_k, d_v_=d_v, d_k=d_k, d_v=d_v, d_o=d_o)


self.init_parameters()


def init_parameters(self):
for param in self.parameters():
stdv = 1. / np.power(param.size(-1), 0.5)
param.data.uniform_(-stdv, stdv)


def forward(self, x, mask=None):
q = torch.matmul(x, self.wq)
k = torch.matmul(x, self.wk)
v = torch.matmul(x, self.wv)


attn, output = self.mha(q, k, v, mask=mask)


return attn, output




if __name__ == "__main__":
n_x = 4
d_x = 80


x = torch.randn(batch, n_x, d_x)
mask = torch.zeros(batch, n_x, n_x).bool()


selfattn = SelfAttention(n_head=8, d_k=128, d_v=64, d_x=80, d_o=80)
attn, output = selfattn(x, mask=mask)


print(attn.size())
print(output.size())

基础机器学习算法篇

手撕 k-means 算法

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
import numpy as np
def kmeans(data, k, thresh=1, max_iterations=100):
# 随机初始化k个中心点
centers = data[np.random.choice(data.shape[0], k, replace=False)]


for _ in range(max_iterations):
# 计算每个样本到各个中心点的距离
distances = np.linalg.norm(data[:, None] - centers, axis=2)


# 根据距离最近的中心点将样本分配到对应的簇
labels = np.argmin(distances, axis=1)


# 更新中心点为每个簇的平均值
new_centers = np.array([data[labels == i].mean(axis=0) for i in range(k)])


# 判断中心点是否收敛,多种收敛条件可选
# 条件1:中心点不再改变
if np.all(centers == new_centers):
break
# 条件2:中心点的阈值小于某个阈值
# center_change = np.linalg.norm(new_centers - centers)
# if center_change < thresh:
# break
centers = new_centers


return labels, centers


# 生成一些随机数据作为示例输入
data = np.random.rand(100, 2) # 100个样本,每个样本有两个特征


# 手动实现K均值算法
k = 3 # 聚类数为3
labels, centers = kmeans(data, k)


# 打印簇标签和聚类中心点
print("簇标签:", labels)
print("聚类中心点:", centers)

手撕 Layer Normalization 算法

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
import torch
from torch import nn

class LN(nn.Module):
# 初始化
def __init__(self, normalized_shape, # 在哪个维度上做LN
eps:float = 1e-5, # 防止分母为0
elementwise_affine:bool = True): # 是否使用可学习的缩放因子和偏移因子
super(LN, self).__init__()
# 需要对哪个维度的特征做LN, torch.size查看维度
self.normalized_shape = normalized_shape # [c,w*h]
self.eps = eps
self.elementwise_affine = elementwise_affine
# 构造可训练的缩放因子和偏置
if self.elementwise_affine:
self.gain = nn.Parameter(torch.ones(normalized_shape)) # [c,w*h]
self.bias = nn.Parameter(torch.zeros(normalized_shape)) # [c,w*h]

# 前向传播
def forward(self, x: torch.Tensor): # [b,c,w*h]
# 需要做LN的维度和输入特征图对应维度的shape相同
assert self.normalized_shape == x.shape[-len(self.normalized_shape):] # [-2:]
# 需要做LN的维度索引
dims = [-(i+1) for i in range(len(self.normalized_shape))] # [b,c,w*h]维度上取[-1,-2]维度,即[c,w*h]
# 计算特征图对应维度的均值和方差
mean = x.mean(dim=dims, keepdims=True) # [b,1,1]
mean_x2 = (x**2).mean(dim=dims, keepdims=True) # [b,1,1]
var = mean_x2 - mean**2 # [b,c,1,1]
x_norm = (x-mean) / torch.sqrt(var+self.eps) # [b,c,w*h]
# 线性变换
if self.elementwise_affine:
x_norm = self.gain * x_norm + self.bias # [b,c,w*h]
return x_norm

# ------------------------------- #
# 验证
# ------------------------------- #

if __name__ == '__main__':

x = torch.linspace(0, 23, 24, dtype=torch.float32) # 构造输入层
x = x.reshape([2,3,2*2]) # [b,c,w*h]
# 实例化
ln = LN(x.shape[1:])
# 前向传播
x = ln(x)
print(x.shape)

手撕 Batch Normalization 算法

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
class MyBN:
def __init__(self, momentum=0.01, eps=1e-5, feat_dim=2):
"""
初始化参数值
:param momentum: 动量,用于计算每个batch均值和方差的滑动均值
:param eps: 防止分母为0
:param feat_dim: 特征维度
"""
# 均值和方差的滑动均值
self._running_mean = np.zeros(shape=(feat_dim, ))
self._running_var = np.ones((shape=(feat_dim, ))
# 更新self._running_xxx时的动量
self._momentum = momentum
# 防止分母计算为0
self._eps = eps
# 对应Batch Norm中需要更新的beta和gamma,采用pytorch文档中的初始化值
self._beta = np.zeros(shape=(feat_dim, ))
self._gamma = np.ones(shape=(feat_dim, ))


def batch_norm(self, x):
"""
BN向传播
:param x: 数据
:return: BN输出
"""
if self.training:
x_mean = x.mean(axis=0)
x_var = x.var(axis=0)
# 对应running_mean的更新公式
self._running_mean = (1-self._momentum)*x_mean + self._momentum*self._running_mean
self._running_var = (1-self._momentum)*x_var + self._momentum*self._running_var
# 对应论文中计算BN的公式
x_hat = (x-x_mean)/np.sqrt(x_var+self._eps)
else:
x_hat = (x-self._running_mean)/np.sqrt(self._running_var+self._eps)
return self._gamma*x_hat + self._beta

解码算法篇

手撕 贪心搜索 (greedy search)

贪心搜索(greedy search)在每个时间步 t 都选取当前概率分布中概率最大的词,即
image.jpg
直到 yt 为或达到预设最大长度时停止生成。
贪心搜索本质上是局部最优策略,但并不能保证最终结果一定是全局最优的。由于贪心搜索在解码的任意时刻只保留一条候选序列,所以在搜索效率上,贪心搜索的复杂度显著低于穷举搜索。

1
2
3
4
5
6
7
8
9
10
11
def greedy_decoding(input_ids, max_tokens=300):
with torch.inference_mode():
for _ in range(max_tokens):
outputs = model(input_ids)
next_token_logits = outputs.logits[:, -1, :]
next_token = torch.argmax(next_token_logits, dim=-1)
if next_token == tokenizer.eos_token_id:
break
input_ids = torch.cat([input_ids, rearrange(next_token, 'c -> 1 c')], dim=-1)
generated_text = tokenizer.decode(input_ids[0])
return generated_text

手撕 Top-K Sampling算法

Top-K 采样(在每个时间步选择条件概率排名前 K 的词语,然后在这 K 个词语中进行随机采样。这种方法既能保持一定的生成质量,又能增加文本的多样性,并且可以通过限制候选词语的数量来控制生成文本的多样性。
这个过程使得生成的文本在保持一定的生成质量的同时,也具有一定的多样性,因为在候选词语中仍然存在一定的竞争性。

1
2
3
4
5
6
7
8
9
10
11
12
def top_k_sampling(input_ids, max_tokens=100, top_k=50, temperature=1.0):
for _ in range(max_tokens):
with torch.inference_mode():
outputs = model(input_ids)
next_token_logits = outputs.logits[:, -1, :]
top_k_logits, top_k_indices = torch.topk(next_token_logits, top_k)
top_k_probs = F.softmax(top_k_logits / temperature, dim=-1)
next_token_index = torch.multinomial(top_k_probs, num_samples=1)
next_token = top_k_indices.gather(-1, next_token_index)
input_ids = torch.cat([input_ids, next_token], dim=-1)
generated_text = tokenizer.decode(input_ids[0])
return generated_text

神经网络篇

手撕 卷积神经网络(CNN)法

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
import torch
import torch.nn.functional as F #使用functional中的ReLu激活函数


#CNN模型
class CNNNet(torch.nn.Module):
def __init__(self):
super(CNNNet, self).__init__()
#两个卷积层
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5) #1为in_channels 10为out_channels
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
#池化层
self.pooling = torch.nn.MaxPool2d(2) #2为分组大小2*2
#全连接层 320 = 20 * 4 * 4
self.fc = torch.nn.Linear(320, 10)


def forward(self, x):
#先从x数据维度中得到batch_size
batch_size = x.size(0)
#卷积层->池化层->激活函数
x = F.relu(self.pooling(self.conv1(x)))
x = F.relu(self.pooling(self.conv2(x)))
x = x.view(batch_size, -1) #将数据展开,为输入全连接层做准备
x = self.fc(x)
return x
model = CNNNet()

手撕 二维卷积 算法

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
import numpy as np 
def conv2d(img, in_channels, out_channels ,kernels, bias, stride=1, padding=0):
N, C, H, W = img.shape
kh, kw = kernels.shape
p = padding
assert C == in_channels, "kernels' input channels do not match with img"


if p:
img = np.pad(img, ((0,0),(0,0),(p,p),(p,p)), 'constant') # padding along with all axis


out_h = (H + 2*padding - kh) // stride + 1
out_w = (W + 2*padding - kw) // stride + 1


outputs = np.zeros([N, out_channels, out_h, out_w])
# print(img)
for n in range(N):
for out in range(out_channels):
for i in range(in_channels):
for h in range(out_h):
for w in range(out_w):
for x in range(kh):
for y in range(kw):
outputs[n][out][h][w] += img[n][i][h * stride + x][w * stride + y] * kernels[x][y]
if i == in_channels - 1:
outputs[n][out][:][:] += bias[n][out]
return outputs

位置编码篇

手撕 绝对位置编码 算法

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
class SinPositionEncoding(nn.Module):
def __init__(self, max_sequence_length, d_model, base=10000):
super().__init__()
self.max_sequence_length = max_sequence_length
self.d_model = d_model
self.base = base


def forward(self):
pe = torch.zeros(self.max_sequence_length, self.d_model, dtype=torch.float) # size(max_sequence_length, d_model)
exp_1 = torch.arange(self.d_model // 2, dtype=torch.float) # 初始化一半维度,sin位置编码的维度被分为了两部分
exp_value = exp_1 / (self.d_model / 2)


alpha = 1 / (self.base ** exp_value) # size(dmodel/2)
out = torch.arange(self.max_sequence_length, dtype=torch.float)[:, None] @ alpha[None, :] # size(max_sequence_length, d_model/2)
embedding_sin = torch.sin(out)
embedding_cos = torch.cos(out)


pe[:, 0::2] = embedding_sin # 奇数位置设置为sin
pe[:, 1::2] = embedding_cos # 偶数位置设置为cos
return pe


SinPositionEncoding(d_model=4, max_sequence_length=10, base=10000).forward()

手撕 可学习位置编码 算法

1
2
3
4
5
6
7
8
9
10
11
class TrainablePositionEncoding(nn.Module):
def __init__(self, max_sequence_length, d_model):
super().__init__()
self.max_sequence_length = max_sequence_length
self.d_model = d_model


def forward(self):
pe = nn.Embedding(self.max_sequence_length, self.d_model)
nn.init.constant(pe.weight, 0.)
return pe

手撕 相对位置编码 算法

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
class RelativePosition(nn.Module):
def __init__(self, num_units, max_relative_position):
super().__init__()
self.num_units = num_units
self.max_relative_position = max_relative_position
self.embeddings_table = nn.Parameter(torch.Tensor(max_relative_position * 2 + 1, num_units))
nn.init.xavier_uniform_(self.embeddings_table)


def forward(self, length_q, length_k):
range_vec_q = torch.arange(length_q)
range_vec_k = torch.arange(length_k)
distance_mat = range_vec_k[None, :] - range_vec_q[:, None]
distance_mat_clipped = torch.clamp(distance_mat, -self.max_relative_position, self.max_relative_position)
final_mat = distance_mat_clipped + self.max_relative_position
final_mat = torch.LongTensor(final_mat).cuda()
embeddings = self.embeddings_table[final_mat].cuda()


return embeddings


class RelativeMultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads, dropout=0.1, batch_size=6):
"Take in model size and number of heads."
super(RelativeMultiHeadAttention, self).__init__()
self.d_model = d_model
self.n_heads = n_heads
self.batch_size = batch_size


assert d_model % n_heads == 0
self.head_dim = d_model // n_heads


self.linears = _get_clones(nn.Linear(d_model, d_model), 4)
self.dropout = nn.Dropout(p=dropout)
self.relative_position_k = RelativePosition(self.head_dim, max_relative_position=16)
self.relative_position_v = RelativePosition(self.head_dim, max_relative_position=16)


self.scale = torch.sqrt(torch.FloatTensor([self.head_dim])).cuda()


def forward(self, query, key, value):
# embedding
# query, key, value = [batch_size, len, hid_dim]
query, key, value = [l(x).view(self.batch_size, -1, self.d_model) for l, x in
zip(self.linears, (query, key, value))]


len_k = query.shape[1]
len_q = query.shape[1]
len_v = value.shape[1]


# Self-Attention
# r_q1, r_k1 = [batch_size, len, n_heads, head_dim]
r_q1 = query.view(self.batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
r_k1 = key.view(self.batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
attn1 = torch.matmul(r_q1, r_k1.permute(0, 1, 3, 2))


r_q2 = query.permute(1, 0, 2).contiguous().view(len_q, self.batch_size * self.n_heads, self.head_dim)
r_k2 = self.relative_position_k(len_q, len_k)
attn2 = torch.matmul(r_q2, r_k2.transpose(1, 2)).transpose(0, 1)
attn2 = attn2.contiguous().view(self.batch_size, self.n_heads, len_q, len_k)
attn = (attn1 + attn2) / self.scale


attn = self.dropout(torch.softmax(attn, dim=-1))
# attn = [batch_size, n_heads, len, len]
r_v1 = value.view(self.batch_size, -1, self.n_heads, self.head_dim).permute(0, 2, 1, 3)
weight1 = torch.matmul(attn, r_v1)
r_v2 = self.relative_position_v(len_q, len_v)
weight2 = attn.permute(2, 0, 1, 3).contiguous().view(len_q, self.batch_size * self.n_heads, len_k)
weight2 = torch.matmul(weight2, r_v2)
weight2 = weight2.transpose(0, 1).contiguous().view(self.batch_size, self.n_heads, len_q, self.head_dim)


x = weight1 + weight2
# x = [batch size, n heads, query len, head dim]


x = x.permute(0, 2, 1, 3).contiguous()
# x = [batch size, query len, n heads, head dim]


x = x.view(self.batch_size * len_q, self.d_model)
# x = [batch size * query len, hid dim]


return self.linears[-1](x)

手撕 rope 算法

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
109
110
111
112
113
114
115
116
117
import torch
import torch.nn as nn
import torch.nn.functional as F
import math


# %%


def sinusoidal_position_embedding(batch_size, nums_head, max_len, output_dim, device):
# (max_len, 1)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(-1)
# (output_dim//2)
ids = torch.arange(0, output_dim // 2, dtype=torch.float) # 即公式里的i, i的范围是 [0,d/2]
theta = torch.pow(10000, -2 * ids / output_dim)


# (max_len, output_dim//2)
embeddings = position * theta # 即公式里的:pos / (10000^(2i/d))


# (max_len, output_dim//2, 2)
embeddings = torch.stack([torch.sin(embeddings), torch.cos(embeddings)], dim=-1)


# (bs, head, max_len, output_dim//2, 2)
embeddings = embeddings.repeat((batch_size, nums_head, *([1] * len(embeddings.shape)))) # 在bs维度重复,其他维度都是1不重复


# (bs, head, max_len, output_dim)
# reshape后就是:偶数sin, 奇数cos了
embeddings = torch.reshape(embeddings, (batch_size, nums_head, max_len, output_dim))
embeddings = embeddings.to(device)
return embeddings


# %%
def RoPE(q, k):
# q,k: (bs, head, max_len, output_dim)
batch_size = q.shape[0]
nums_head = q.shape[1]
max_len = q.shape[2]
output_dim = q.shape[-1]


# (bs, head, max_len, output_dim)
pos_emb = sinusoidal_position_embedding(batch_size, nums_head, max_len, output_dim, q.device)


# cos_pos,sin_pos: (bs, head, max_len, output_dim)
# 看rope公式可知,相邻cos,sin之间是相同的,所以复制一遍。如(1,2,3)变成(1,1,2,2,3,3)
cos_pos = pos_emb[..., 1::2].repeat_interleave(2, dim=-1) # 将奇数列信息抽取出来也就是cos 拿出来并复制
sin_pos = pos_emb[..., ::2].repeat_interleave(2, dim=-1) # 将偶数列信息抽取出来也就是sin 拿出来并复制


# q,k: (bs, head, max_len, output_dim)
q2 = torch.stack([-q[..., 1::2], q[..., ::2]], dim=-1)
q2 = q2.reshape(q.shape) # reshape后就是正负交替了


# 更新qw, *对应位置相乘
q = q * cos_pos + q2 * sin_pos


k2 = torch.stack([-k[..., 1::2], k[..., ::2]], dim=-1)
k2 = k2.reshape(k.shape)
# 更新kw, *对应位置相乘
k = k * cos_pos + k2 * sin_pos


return q, k


# %%
def attention(q, k, v, mask=None, dropout=None, use_RoPE=True):
# q.shape: (bs, head, seq_len, dk)
# k.shape: (bs, head, seq_len, dk)
# v.shape: (bs, head, seq_len, dk)


if use_RoPE:
q, k = RoPE(q, k)


d_k = k.size()[-1]


att_logits = torch.matmul(q, k.transpose(-2, -1)) # (bs, head, seq_len, seq_len)
att_logits /= math.sqrt(d_k)


if mask is not None:
att_logits = att_logits.masked_fill(mask == 0, -1e9) # mask掉为0的部分,设为无穷大


att_scores = F.softmax(att_logits, dim=-1) # (bs, head, seq_len, seq_len)


if dropout is not None:
att_scores = dropout(att_scores)


# (bs, head, seq_len, seq_len) * (bs, head, seq_len, dk) = (bs, head, seq_len, dk)
return torch.matmul(att_scores, v), att_scores


if __name__ == '__main__':
# (bs, head, seq_len, dk)
q = torch.randn((8, 12, 10, 32))
k = torch.randn((8, 12, 10, 32))
v = torch.randn((8, 12, 10, 32))


res, att_scores = attention(q, k, v, mask=None, dropout=None, use_RoPE=True)


# (bs, head, seq_len, dk), (bs, head, seq_len, seq_len)
print(res.shape, att_scores.shape)

面试题汇总

致谢

来自: 算法工程师面试常考手撕题(更新)