ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

旷视研发工程师笔试复盘:算法、机器学习与系统设计核心考点解析

旷视研发工程师笔试复盘:算法、机器学习与系统设计核心考点解析 春招那阵子我投了不少算法岗旷视的笔试算是印象比较深的一场。不说别的单是“研发工程师”这个岗位的笔试范围就比想象中广算法、概率、机器学习基础、甚至一点工程细节都会涉及。虽然这是2019年的题但从面试角度来说它考察的核心逻辑到现在也没怎么变基础扎不扎实、代码能不能写利索、思路会不会转弯。这篇文章就结合当年的笔试经历把整套题目的考察逻辑、重要考点、以及我当时踩过的坑完整拆一遍给准备类似岗位笔试的同学一个参考。1. 笔试整体设计与思路拆解1.1 为什么笔试会这么出题旷视做的是计算机视觉方向但这不意味着笔试开篇就考卷积神经网络。2019年春招这场研发工程师笔试整体来看还是以计算机基础为主算法题占大头机器学习相关的题作为拉开差距的部分。这个出题思路其实很现实视觉算法工程师首先得是合格的工程师代码底子和逻辑能力不过关后面做模型训练、部署、数据管线都容易出问题。整张卷子的结构大致可以分成四块不定项选择、算法编程题、简答/推导题、以及一个小型系统设计题。选择部分涵盖数据结构、操作系统、网络、概率统计题目不算难但面铺得很开。编程题通常是两道一道偏数据结构一道偏动态规划或搜索。简答题考机器学习基础概念比如正则化、过拟合、交叉验证这些偶尔会让你推一下某个损失函数的梯度。最后的系统设计题是拉分项给一个场景让你设计解决方案。这套设计思路其实是多数AI公司技术笔试的标准套路核心目标是在两个小时内快速筛选出“基础过关、能动手写代码、有一定系统思维”的人。所以准备这类笔试不需要去刷特别偏难怪的题但计算机核心课程的地基必须打得足够稳。1.2 从岗位要求反推笔试重点研发工程师在旷视的语境里更偏算法落地方向。也就是说光会调模型不行你得能处理数据、写训练脚本、调参、部署服务甚至要会优化推理速度。这决定了对工程能力的考察不会少。笔试中体现得很明显编程题不是LeetCode那种纯刷题模式而是带着实际场景味道比如在二维矩阵里找最长的满足某种性质的路径或者在资源受限的情况下做任务调度。这类题背后考察的不只是语法熟练度更是问题建模能力。另外像概率题出现在选择里也说明他们希望你有扎实的数理基础毕竟做模型评估、A/B测试、采样策略这些事情不懂概率是玩不转的。2. 核心考点解析与实操要点2.1 算法题重点LeetCode中等题为主不要死磕难题从实际题目的难度分布来看笔试编程题的难度基本集中在LeetCode中等题偶尔会出现一道偏简单的困难题。涵盖的题型无非就是数组操作、链表、二叉树遍历、动态规划、回溯搜索、双指针、哈希表。这些类别看起来很多但高频考点是非常集中的。我当时遇到的编程题一道是“给定一个整数矩阵从左上角走到右下角每次只能向右或向下求路径上数字之和最大的路线”的变体——多加了一个条件某些格子不能走。这就是典型的动态规划问题但是加入了障碍物后需要初始化的时候特别注意边界情况。另一道是“实现一个LRU缓存”要求get和put都是O(1)时间复杂度。这题看着简单实际坑不少需要同时用哈希表和双向链表而且链表节点的前后指针操作特别容易绕晕。这类题目的准备思路我建议把常见的数据结构操作练成肌肉记忆比如链表反转、二叉树的前中后序遍历、快速排序和归并排序的手写、二分查找的边界处理、 HashMap的底层原理。这些基本功扎实了笔试中遇到的大部分题都能找到思路。2.2 机器学习基础不只是背概念要能推导简答和选择里涉及的机器学习内容主要集中在几个方向正则化的作用和原理L1为什么会产生稀疏解、过拟合的判别与应对方法、交叉验证的流程、梯度下降的几种变体SGD、Momentum、Adam的区别、损失函数的选择、样本不均衡的处理方式。表面上是概念题但如果你只看书没有自己推过一遍很容易在“L1正则化为什么会产生稀疏解”这种题上卡住。这个问题的核心在于L1正则化的约束区域是菱形在二维情形下最优解更容易落在坐标轴上从而实现稀疏。但用文字回答和用公式推导完全是两回事笔试的简答题要求你写出数学表达式和推导过程平时不动手推到考场上很难写出完整答案。我记得当时有一道题是“写出逻辑回归的损失函数并推导梯度”这题其实不难但如果平时只是背结论推导过程中很容易在sigmoid函数求导那一步出错。sigmoid的一个重要性质是σ′(x)σ(x)(1−σ(x))利用这个性质可以让梯度表达式变得很简洁这个技巧我建议提前练熟。2.3 数学与概率统计不要忽视的基础分笔试选择里混着几道概率统计题看起来不起眼但往往是区分度很高的部分。常见考法有给你一个随机变量的分布函数求期望和方差或者给你一个贝叶斯公式的场景题让你计算后验概率。举个例子一类很经典的题是这样的某疾病的患病率为1%检测方法的灵敏度是99%特异度是95%如果一个人检测结果为阳性问实际患病的概率是多少。这题直接用贝叶斯公式算答案其实只有不到17%很多凭直觉选的人都会选错。这类题的分很好拿考前把条件概率、贝叶斯公式、常见分布正态分布、二项分布、泊松分布的均值和方差公式过一遍基本就能拿到分。2.4 系统设计题考察的是工程思维最后那道系统设计题考的是“给一个人脸识别门禁系统设计整体架构要求说明各个模块的职责、可能的瓶颈和优化方案”。这种题不是让你写出代码而是看你有没有能力把一个实际业务问题拆解成可实现的组件。答题思路大概是这样的第一先明确系统的核心流程——人脸检测、特征提取、特征比对、结果输出第二在每个流程上说明清楚选什么技术方案比如人脸检测用MTCNN还是RetinaFace特征提取用ResNet50还是MobileNet系列这里需要考虑精度的同时也要评估部署资源第三一定要提到性能优化比如模型量化剪枝、推理引擎选型、缓存策略等第四说明数据存储方案比如特征向量库用faiss这类向量检索工具而不需要提具体的数据库品牌关键是说明为什么它适合这个场景。这类题没有标准答案但能看出一个人是不是真实做过项目。如果完全没接触过工程化部署写出来的方案很容易停留在理论层面所以在准备这类岗位时一定要自己去跑一遍完整的模型部署流程哪怕在本地跑通一个简化版也很有帮助。3. 实操过程与核心环节实现3.1 动态规划题从暴力递归到状态压缩的完整演进笔试遇到动态规划题最怕的不是不会做而是上来就写了一个错误的贪心。我建议在平时练习时就养成一套固定的解题节奏先明确状态定义再写状态转移方程然后初始化边界最后考虑能否优化空间复杂度。以“带障碍物的二维矩阵最大路径和”为例我们一步步来推演第一步定义状态。设 dp[i][j] 表示从起点走到 (i,j) 位置时的最大路径和。这个定义非常直观也是大多数二维动态规划问题的通用状态。第二步写状态转移方程。因为只能向右或向下走所以 (i,j) 位置的路径只可能来自上方 (i−1,j) 或左方 (i,j−1)转移方程为 dp[i][j] grid[i][j] max(dp[i−1][j], dp[i][j−1])第三步初始化边界。第一行的格子只能从左边走过来第一列的格子只能从上面走过来。但这里需要特别注意一个坑如果某个格子是障碍物那么不仅这个格子本身不可达它后面的一整行或一整列也不可达了。这就是笔试里容易忽略的地方。第四步空间优化。仔细观察转移方程可以发现dp[i][j] 只依赖上一行的数据所以可以用一维数组来滚动更新vectorint dp(cols, 0); for (int i 0; i rows; i) { for (int j 0; j cols; j) { if (grid[i][j] -1) { dp[j] -1; // 障碍物标记 } else { int left (j 0 dp[j-1] ! -1) ? dp[j-1] : INT_MIN; int up (dp[j] ! -1) ? dp[j] : INT_MIN; if (i 0 j 0) dp[j] grid[0][0]; else if (left INT_MIN up INT_MIN) dp[j] INT_MIN; else dp[j] grid[i][j] max(left, up); } } }这题的核心难点在于状态定义只要想清楚“走到当前位置的最大路径和”后面写代码只是水到渠成。笔试现场不需要追求写出最优的滚动数组版本二维dp能AC通过全部测试用例就已经能拿大部分分数了。3.2 LRU缓存哈希表加双向链表的标准解法LRU缓存机制是经典面试题笔试里如果出现通常要求实现 get 和 put 两个操作时间复杂度为O(1)。判断一个候选人是不是真的理解这题关键看他能不能解释清楚“为什么要哈希表双向链表”而不只是背代码。哈希表负责实现O(1)的查找而双向链表负责维护访问顺序。链表头表示最近访问的节点链表尾表示最久未访问的节点。每次 get 一个 key就把对应节点移到链表头部每次 put 一个新 key也插入到头部如果缓存超过容量就删除尾部节点并同步删除哈希表中的记录。这里有个非常容易踩的坑如果用单链表删除尾部节点需要遍历链表才能找到它的前驱节点时间复杂度就变成O(n)了。双向链表则可以直接通过节点的 prev 指针找到前驱达到O(1)删除。这个细节非常关键有些同学在面试官追问为什么不用单链表时答不上来就是没有理解数据结构选型的本质。我当时笔试里写的大致代码结构是这样的class LRUCache { private: struct Node { int key, value; Node* prev; Node* next; Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {} }; int capacity; Node* head; Node* tail; unordered_mapint, Node* mp; void removeNode(Node* node) { node-prev-next node-next; node-next-prev node-prev; } void addToHead(Node* node) { node-next head-next; node-prev head; head-next-prev node; head-next node; } public: LRUCache(int capacity) { this-capacity capacity; head new Node(0, 0); tail new Node(0, 0); head-next tail; tail-prev head; } int get(int key) { if (mp.find(key) mp.end()) return -1; Node* node mp[key]; removeNode(node); addToHead(node); return node-value; } void put(int key, int value) { if (mp.find(key) ! mp.end()) { Node* node mp[key]; node-value value; removeNode(node); addToHead(node); } else { if (mp.size() capacity) { Node* last tail-prev; removeNode(last); mp.erase(last-key); delete last; } Node* node new Node(key, value); mp[key] node; addToHead(node); } } };代码本身并不长但链表操作非常容易绕晕。我当时的做法是全程在纸上画节点图先画出 head、tail 和实际节点之间的连接关系再在图上模拟一次插入和删除最后再去写代码。这招在笔试现场非常管用能减少很多低级错误。3.3 逻辑回归梯度推导公式推导的拿分技巧简答题里那道“写出逻辑回归的损失函数并推导梯度”我在这里把完整过程走一遍你会发现它没有想象的那么复杂。逻辑回归中对于单个样本 (x,y)预测概率为 hθ(x)P(y1|x;θ)σ(θ^T x)其中σ是sigmoid函数。用极大似然估计单个样本的损失可以写成 L(θ) −[ y log(hθ(x)) (1−y) log(1−hθ(x)) ]这是二分类交叉熵的单个样本形式。接下来求参数θ的梯度关键是利用sigmoid函数的导数性质σ′(z)σ(z)(1−σ(z))。先看对log(hθ(x))求梯度 ∂ log(hθ(x)) / ∂θ (1/hθ(x)) * hθ(x)(1−hθ(x)) * x (1−hθ(x)) * x再看对log(1−hθ(x))求梯度 ∂ log(1−hθ(x)) / ∂θ −(1/(1−hθ(x))) * hθ(x)(1−hθ(x)) * x −hθ(x) * x把两部分代入损失函数的梯度表达式 ∂L/∂θ −[ y(1−hθ(x))x − (1−y)hθ(x)x ] [ y hθ(x) − yhθ(x) − hθ(x) yhθ(x) ] * x (hθ(x) − y) * x最终梯度就是预测值与真实标签的差再乘以特征向量这个形式简洁到令人惊讶。如果在推导过程中利用好sigmoid的导数性质整个过程非常顺畅。如果硬算sigmoid的导数而不做化简很容易在代数运算中出错。这道题拿到满分的关键不是记住最终结果而是把中间步骤写清楚尤其是sigmoid求导那一步。4. 常见问题与排查技巧实录4.1 编程题常见失误边界条件与输入输出笔试编程题最常见的失误其实不是逻辑错误而是边界条件处理不完整。数组越界、空数组、只有一行或只有一列的矩阵、整数溢出这些场景都是测试用例中一定会出现的情况。以动态规划题为例如果矩阵只有一行那么dp数组的初始化就要考虑到“只能向右走”如果没有障碍物问题退化为普通路径问题但你的代码仍然要能正确处理。这些都是非常细节的地方但在笔试的线上测试环境里几乎全部都会作为隐藏测试用例。另外一个容易出问题的地方是输入输出格式。有些笔试平台会要求你处理循环输入有些是一次性读入全部数据。我当时的建议是提前去了解笔试平台常用的输入方式在本地练习时就用标准输入输出。千万别在考场上花时间研究怎么从标准输入读取数据那是白白浪费时间。4.2 简答题常见误区只写结论不写过程简答题的判分方式通常按步骤给分即使最终结果错误中间的推导步骤正确也能拿到不少分。但很多同学的习惯是直接写一个最终结论过程一句带过甚至完全省略这在笔试中非常吃亏。比如让你推导L1正则化产生稀疏解的原因正确的答题方式应该是先写出L1正则化后的损失函数再画出约束区域和等值线的几何解释最后说明凸优化中尖角导致的稀疏性。每一步都有分而仅仅写一句“L1能得到稀疏解”是拿不到分的。每道简答题的答题结构大致是先给出结论再用公式或图形说明原因最后补充一个例子或特殊情况。这套结构在笔试和面试中都非常实用建议平时多练习。4.3 笔试时间分配策略分值优先不要死磕整张卷子两个小时编程题通常给的时间最多但不要一上来就钻进编程题。我的分配策略是先花10分钟左右浏览全部题目心里快速判断每道题的难度和预计时间。选择填空题能快速拿分的先做简答题的关键词先写在草稿纸上编程题如果超过20分钟没有思路先跳过做后面的题目最后再回头想。这里想强调一个很现实的技巧在线上笔试环境中编程题只要逻辑正确不要求代码风格多优雅甚至不用考虑代码重复和内存效率能AC就是胜利。但简答题如果空着就真的是零分。安排好时间确保每道题都有回答是笔试的基本策略。4.4 关于智商题和偏题的准备有些人会担心笔试里出现“奇怪的智商题”或者纯脑筋急转弯式的题目。从我实际体验来看这类题即使偶尔出现占比也极低核心考察还是基础和思维逻辑。与其花大量时间准备偏题怪题不如把计算机基础课重新过一遍数据结构、操作系统、计算机网络、概率统计。操作系统和网络的基础知识在选择题里经常出现比如进程和线程的区别、死锁产生的必要条件、TCP三次握手的过程、IP数据包的分片规则等。这些内容虽然不直接关联视觉算法但作为一名研发工程师它们是通用常识。把这些基础打牢选择题能多拿不少分。5. 笔试之外的个人体会我后来复盘这段笔试经历时发现准备过程和最终结果之间并没有那么强的线性关系。真正有用的其实是笔试前的系统复习它逼着我把大学期间的知识点重新串了一遍也让我意识到工程能力在算法岗位中的权重越来越高。如果你正在准备类似岗位的笔试我个人最想强调的就三点第一算法题做熟练不等于会做一定要理解每个数据结构选型背后的原因第二推导题要动手写不要只在脑子里想第三平时项目训练中尽量多走一遍从数据到部署的完整流程这个经历在笔试和面试里都会无形中帮到你。这些内容放在今天来看依然不会过时。笔试只是求职的一小步但它考察的东西恰恰是研发工程师日常工作中最需要的底层能力。希望这篇复盘对你有用。
返回列表