ARTICLE · 1226175
华为机考AI方向10月21日笔试题与解析
写在前面
本次给大家带来2026年10月21日华为AI方向笔试题的2道题,本套题涉及到的岗位:AI算法工程师,AI应用开发工程师,AI数据科学工程师等以AI为开头的岗位,统一笔试,不区分部门。
需要我整理的AI机考速成攻略&题单可访问文章底部左侧:阅读原文
选择题(15*6分 + 5*12分,共150分)
1、只有一个输出神经元的二分类模型,要把它的输出解释成“属于正类的概率”,最后一层应接哪个激活函数?
ReLU Softmax Sigmoid Tanh
2、回归任务中混进了少量误差极大的离群点,下面关于 MSE 与 MAE 的说法,正确的是?
绝对值损失 MAE 对离群点更敏感 平方损失 MSE 对离群点更敏感 平方损失 MSE 在误差为 0 处不可导 绝对值损失 MAE 的收敛速度一般快于 MSE
3、缩放点积注意力里把 除以 ,这样做的目的是什么?
把单头计算拆成多头并行 降低矩阵乘法的计算量 增加模型可训练参数 避免内积过大使 softmax 饱和、梯度接近 0
4、某推理场景里有 6 个逐元素的小算子需要串行执行。每次向 GPU 下发 kernel 的固定开销为 ,每个算子在 GPU 上的纯执行时间为 ,忽略访存优化。不融合时总耗时记为 ;用编译器把它们合并成一个 kernel 后总耗时记为 。则 约为多少?
1.5 倍 2 倍 3 倍 3.6 倍
5、RAG 流程中,向量检索这一步最终要拿到的东西是什么?
对问题做分词并截断到固定长度 从库里取出与问题语义最接近的文档片段 由生成模型据此写出最终答案 只要算出一个相似度分数就算完成
6、用 Self-Instruct 方法自动构造指令数据,起步时需要准备的是什么?
一个已经对齐好的奖励模型 一批带标注的测试集 大规模无标注图像 一小批人工编写的种子指令
7、设 是 维向量空间, 是 的子空间,则 最大能取到多少?
8、用 ViT 做视觉编码器时,常把切出的视觉 token 再做一次压缩,其主要目的是?
提升输入图像的原始分辨率 让图像语义自动变准确 降低后续计算量与显存占用 增加视觉侧可训练参数量
9、关于 t-SNE,下面说法正确的是?
它是非线性方法,主要保留高维数据的局部邻域结构 它属于线性降维,结果对特征缩放敏感 它计算量很小,百万级样本可以随意直接跑 它每次运行结果完全随机,没有参考价值
10、PagedAttention 缓解 KV Cache 显存碎片化的做法是?
把 KV Cache 压缩为 INT8 低精度,靠降位宽省显存 丢弃部分早期 KV Cache,靠释放显存腾出空间 每步重算 KV 向量,靠减少存储量避免碎片 把 KV Cache 切成固定大小的页,让离散空闲块拼成连续页块使用
第2题-用决策树判断玩家购买意向(150分)
一款 5v5 竞技手游每逢版本更新都会上新皮肤。商店推荐位如果不加区分地把所有皮肤推给所有玩家,玩家划得累,购买转化率也上不去。项目组的做法是:先从历史数据里攒一批"看过页面、最终买没买"的记录,用决策树把购买意向学出来,再把新玩家逐条喂进这棵树做预测。
购买意向只用三个布尔特征描述,编号为 0、1、2:
训练样本的类别也只有两种:0 表示不购买(不推荐),1 表示购买(推荐)。
决策树的构建规则
熵
对只含两类的样本集 ,记类别 0、1 的样本占比为 、(),熵定义为
其中 的项按 处理。
条件熵与信息增益
用特征 把 分成 (取值为 0 的样本)与 (取值为 1 的样本),分裂后的条件熵是两棵子树熵按样本量加权平均:
特征 的信息增益,是分裂前后不确定性的减少量:
节点分裂
从根节点开始递归建树。在当前节点上,对尚未出现在从根到该节点的路径上的特征逐个计算信息增益,取增益最大的特征作为分裂特征;若几个特征的增益并列最大,取编号最小的那一个。然后按该特征的取值把节点内的样本分成两类:取值为 0 的进左子树,取值为 1 的进右子树。
某个特征一旦在某条路径上用过了,就不能在这条路径的下层节点上再用;但在另一条分支上它仍然可用。
终止条件
满足下列任意一条,当前节点就成为叶节点,不再分裂:
节点内所有样本的类别相同; 三个特征都已经在从根到该节点的路径上出现过; 所有尚未使用的特征,其信息增益都不大于 0。
条件 1 的叶节点,类别就是节点内样本的类别;条件 2、3 的叶节点按多数表决定类:节点内类别 1 的样本多于类别 0 就取 1,少于就取 0,两者数量相等时取 0。
预测
对一条待预测样本,从根节点出发,读出当前节点分裂特征的取值:0 走左子树,1 走右子树,直到落在叶节点,输出该叶节点的类别。
样例1
输入
5 51 0 1 11 1 0 10 0 1 00 1 1 11 0 0 01 0 11 1 00 0 10 1 11 0 0
输出
11010
解释:训练集共 5 条,类别 0 有 2 条、类别 1 有 3 条,。三个特征的信息增益分别约为 、、,最大的是特征 1,根节点按特征 1 分裂。特征 1 取 0 的一侧有 3 条样本,类别为 1、0、0,此时特征 0 与特征 2 的增益并列(都约为 ),按规则取编号较小的特征 0;其中特征 0 取 1 的两条样本类别仍不同,再按特征 2 分开。特征 1 取 1 的一侧两条样本类别都是 1,直接成为叶节点。5 条待预测样本依次落到类别 1、1、0、1、0 的叶节点上。
解题思路
核心思路是照着题面给的公式把决策树建出来,难点不在算法而在实现细节:特征只在"从根到当前节点"这条路径上禁用、增益并列取编号最小的特征、叶节点多数表决并列时取 0。
训练集只有 3 个布尔特征,决策树的深度最多为 3(一条路径上每个特征最多用一次),所以整棵树的规模是常数级,不需要任何剪枝或优化技巧,按定义递归即可。
实现方法:
读入 条训练样本,每条样本存成 。 递归建树。节点参数是当前节点包含的样本集合与该路径上已用过的特征集合(用一个 3 位掩码表示)。 节点内两类样本数 :若有一类为 0,节点纯,直接成为叶节点,类别取该样本的类别。 否则先算节点熵 。对每个未在路径上出现过的特征 ,统计 的四元计数,按 加权求条件熵,增益 。用"严格大于当前最优值"的方式更新,并把比较换成 :由于特征 0、1、2 是从小到大遍历的,并列时自然保留编号最小的那个。 若没有特征的增益严格为正,说明已经无法再降低不确定性,节点成为叶节点: 取 1,否则取 0(相等时取 0)。 否则按选中的特征把样本分成两类递归下去,两侧的掩码都或上该特征的位。 预测时从根节点开始,看当前节点分裂特征的取值,0 进左子树、1 进右子树,走到叶节点输出类别。
几点容易写错的地方:
掩码只沿路径传递,另一个分支上同一个特征仍然可用,不要做成全局禁用。 纯节点()必须先判断,否则会在没有可用特征时错误地走多数表决。 增益为 0 的特征不能选,否则会无限递归或退化;题面保证被选中的特征在节点内两种取值都出现过,左右子树都不会为空。 多数表决用"大于"而不是"大于等于",两类相等时取 0。
python代码(C++和JAVA代码见在线OJ网址)
import mathEPS = 1e-9def entropy(c0, c1):# 熵 H(S) = -Σ p·log2 p;样本数为 0 的类别不产生贡献 n = c0 + c1 h = 0.0if c0: p = c0 / n h -= p * math.log2(p)if c1: p = c1 / n h -= p * math.log2(p)return hdef build(rows, used):# 统计当前节点的两类样本数,纯节点直接成叶 c1 = sum(r[3] for r in rows) c0 = len(rows) - c1if c0 == 0 or c1 == 0:return rows[0][3] n = len(rows) h = entropy(c0, c1) best = -1 gain = 0.0# 只在路径上没用过的特征里选增益最大的;并列时编号小的先出现,不会被替换for f in range(3):if used >> f & 1:continue cnt = [[0, 0], [0, 0]]for r in rows: cnt[r[f]][r[3]] += 1# 条件熵:两侧子集熵按样本量加权 cond = 0.0for v in (0, 1): t = cnt[v][0] + cnt[v][1]if t: cond += t / n * entropy(cnt[v][0], cnt[v][1]) g = h - condif g > gain + EPS: gain = g best = fif best < 0:# 没有正增益可用:多数表决,两类相等取 0return 1 if c1 > c0 else 0 left = [r for r in rows if r[best] == 0] right = [r for r in rows if r[best] == 1] node = used | 1 << bestreturn (best, build(left, node), build(right, node))def predict(node, x):# 特征取 0 走左子树,取 1 走右子树,直到叶节点while isinstance(node, tuple): node = node[2] if x[node[0]] else node[1]return nodedef main(): n, m = map(int, input().split()) rows = [tuple(map(int, input().split())) for _ in range(n)] root = build(rows, 0)for _ in range(m):print(predict(root, list(map(int, input().split()))))main()
第3题-自动确定簇数的 X-Means 聚类(300分)
K-Means 必须由人给定簇数 K;X-Means 是在其基础上加上 BIC(贝叶斯信息准则),利用算法在候选区间里把 K选出来。
本题要求实现 X-Means:给定 N 个 d 维数据点以及簇数候选区间 [Kmin, Kmax],自动选出最优簇数 K,并输出各簇的中心坐标和每个数据点的簇标签。
算法介绍
K-Means 基础
K-Means 是最经典的无监督聚类算法:把 N 个点划分成 K 个簇,使簇内误差平方和(SSE)尽可能小。
SSE 定义:
也就是把每个点到其所属簇中心的欧氏距离平方全部累加。
算法步骤:
初始化:按确定性策略从数据中挑出 K 个点作为初始簇中心(见文末提示,不允许随机初始化) 分配(Assign):把每个点归到距离最近的簇中心 更新(Update):把每个簇的中心改成该簇内所有点的均值 重复 2、3 两步,直到某一轮迭代结束后所有点的簇标签都不再变化(视为收敛),或迭代轮数达到 100 轮上限
BIC(贝叶斯信息准则)
BIC 用于在若干 K 值对应的聚类结果之间做选择,既衡量拟合程度,也衡量模型复杂度:
参数说明:
理解要点:
BIC 越小越好 第一项 (n\cdot\ln\left(\frac{SSE}{n}\right)) 刻画拟合误差 —— SSE 越小越好 第二项 (p\cdot\ln(n)) 是复杂度惩罚 —— 簇数越多,惩罚越重 簇数变多能压低 SSE,同时抬高惩罚,BIC 在两者之间取平衡
注意事项:
若 K-Means 收敛后簇中心重合而被合并,K 取合并后的实际簇数,而不是枚举时传入的 K 所有点重合导致 SSE=0 时,用 ε=1e-8 兜住 log(0) 若多个 K 算出的 BIC 相同,取其中最小的 K 作为最终结果
X-Means 算法流程
X-Means 的核心是枚举 + BIC 选优:
输入:数据集 X,簇数区间 [Kmin, Kmax]对 K = Kmin, Kmin+1, ..., Kmax 调用 KMeans(X, K),得到 centers, labels, SSE 合并重合的中心,得到实际的 centers, labels, k_actual 计算 BIC = n·ln((SSE + 1e-8)/n) + k_actual·(d+1)·ln(n)选取 BIC 最小的 K 对应的聚类结果作为最终输出
样例1
输入
12 21.0 1.015.0 12.07.0 6.02.0 1.08.0 6.017.0 12.01.0 2.07.0 7.015.0 14.02.0 2.017.0 14.08.0 7.01 5
输出
31.50 1.507.50 6.5016.00 13.000 2 1 0 1 2 0 1 2 0 2 1
解释:12 个二维点明显聚成三小堆,每堆 4 个点,分别围绕 (1.5,1.5)、(7.5,6.5)、(16,13),堆内距离远小于堆间距离。把 K 从 1 枚举到 5,对应 BIC 依次为 56.29、43.86、22.36、29.13、32.90,K=3 时最小,所以最终簇数为 3。三个簇按中心坐标的字典序升序编号:0 号 (1.50,1.50)、1 号 (7.50,6.50)、2 号 (16.00,13.00)。再按输入顺序逐点判定:(1,1) 属于 0 号簇,(15,12) 属于 2 号簇,(7,6) 属于 1 号簇……得到最后一行标签。
解题思路
本题是模拟题:把题面给出的 X-Means 流程逐步实现即可,难点全在细节上。
核心思路是枚举 + 按 BIC 选优:对区间 [Kmin, Kmax] 中的每个 K 跑一次 K-Means,合并重合的簇心,算出 BIC,最后取 BIC 最小的那组结果。
实现方法:
截断:先令 Kmax = min(Kmax, N);若截断后 Kmin > Kmax,再令 Kmin = Kmax,之后枚举 K = Kmin..Kmax。 固定 K 的 K-Means。K=1 时初始簇心取全体点的均值(只有一个簇,结果其实与初始化无关);K≥2 时把点按第一维升序稳定排序,第 i 个初始簇心取排序后下标 的点。排序必须稳定:第一维相同时保持原有相对次序,输出才唯一。 迭代。分配时用 (距离平方, 簇编号) 这一对值比较,天然实现“到多个簇心距离相同时归给编号最小的簇”;若本轮得到的标签与上一轮完全相同就收敛退出;否则按标签重算各簇均值,空簇保留原来的中心。最多迭代 100 轮。 合并重合簇心。两两比较簇心,距离平方小于 的两个簇用并查集并成一簇(合并可传递,代表取组内较小的编号);合并后的簇心是该簇所有点的均值,簇数记作 k',标签重新映射为 0..k'-1。 重编号。把合并后的簇心按坐标字典序升序排列(先比第一维,第一维相同再比第二维),建一张“旧编号 → 新编号”的表,逐点查表得到最终标签,簇心也按新顺序输出,于是同一份数据的输出唯一。 计算 BIC:
其中 SSE 由最终的簇心与最终的标签累加距离平方得到。比较时用严格小于,于是 BIC 相同的候选之间自然保留较小的 K。 7. 输出 k'、各簇心(保留两位小数,绝对值小于 0.005 的坐标置 0,避免出现 -0.00)以及 N 个标签。
容易出错的地方:
BIC 的惩罚项必须用合并后的实际簇数 k',不能写枚举传入的 K。例如所有点重合时,K=2 的两簇中心完全一样,合并后 k'=1,惩罚项也要按 1 算。 合并是传递的:A 与 B 足够近、B 与 C 足够近时三者要并成一个簇,只做两两标记会漏。 标签重排必须在合并之后做,并且按坐标字典序比较;只比第一维时,第一维相同的两个中心会因实现不同而排出不同顺序。 空簇要保留原中心,否则重算均值时会除以 0。 Kmax 可能大于 N,Kmin 也可能大于截断后的 Kmax,这两条截断规则都要处理。
python代码(C++和JAVA代码见在线OJ网址)
import mathINF = 1e308def kmeans(pts, n, d, k):"""对一个固定的 K 跑 K-Means,返回 (簇心, 归属编号)。"""if k == 1: c = [0.0] * dfor i in range(n): p = pts[i]for j in range(d): c[j] += p[j] centers = [[c[j] / n for j in range(d)]]else: order = sorted(range(n), key=lambda i: pts[i][0]) centers = []for i in range(k): centers.append(list(pts[order[i * (n - 1) // (k - 1)]])) lab = [-1] * n# 最多迭代 100 轮for _ in range(100): new = [0] * nfor i in range(n): p = pts[i] bd = INF bi = 0for ci in range(k): c = centers[ci] s = 0.0for j in range(d): t = p[j] - c[j] s += t * t# 距离平方最小;并列时保留编号更小的簇if s < bd: bd = s bi = ci new[i] = bi# 标签一轮都没变,视为收敛if new == lab:break lab = new# 更新:按标签重算簇心,空簇下一轮保持原中心 sums = [[0.0] * d for _ in range(k)] cnt = [0] * kfor i in range(n): ci = lab[i] cnt[ci] += 1 row = sums[ci] p = pts[i]for j in range(d): row[j] += p[j]for ci in range(k):if cnt[ci] > 0: row = sums[ci] c = centers[ci]for j in range(d): c[j] = row[j] / cnt[ci]return centers, labdef merge(pts, n, d, centers, lab, k):"""合并重合簇心,返回 (簇心, 归属编号, 实际簇数)。"""# 两两比较簇心,距离平方小于 1e-4 就并成一簇(并查集,可传递合并) parent = list(range(k))def find(x):while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x]return xfor i in range(k):for j in range(i + 1, k): s = 0.0for t in range(d): u = centers[i][t] - centers[j][t] s += u * uif s < 1e-4: a = find(i) b = find(j)if a != b:if a < b: parent[b] = aelse: parent[a] = b root = [find(i) for i in range(k)] gid = {}for i in range(k):if root[i] not in gid: gid[root[i]] = len(gid) k2 = len(gid) lab2 = [gid[root[x]] for x in lab] sums = [[0.0] * d for _ in range(k2)] cnt = [0] * k2for i in range(n): g = lab2[i] cnt[g] += 1 row = sums[g] p = pts[i]for j in range(d): row[j] += p[j] c2 = [[sums[g][j] / cnt[g] for j in range(d)] for g in range(k2)]# 按簇心坐标的字典序升序重排编号:先比第一维,相同再比第二维 pos = sorted(range(k2), key=lambda g: c2[g]) rank = [0] * k2for t in range(k2): rank[pos[t]] = t final_c = [c2[pos[t]] for t in range(k2)] final_l = [rank[lab2[i]] for i in range(n)]return final_c, final_l, k2def sse_of(pts, n, d, centers, lab): s = 0.0for i in range(n): c = centers[lab[i]] p = pts[i]for j in range(d): t = p[j] - c[j] s += t * treturn sdef fmt(v):# 保留两位小数;绝对值小于 0.005 的坐标按 0 输出,避免出现 -0.00if -0.005 < v < 0.005: v = 0.0return "%.2f" % vdef solve(pts, n, d, kmin, kmax): kmax = min(kmax, n)if kmin > kmax: kmin = kmax best = None best_bic = INFfor k in range(kmin, kmax + 1): centers, lab = kmeans(pts, n, d, k) c2, l2, k2 = merge(pts, n, d, centers, lab, k) sse = sse_of(pts, n, d, c2, l2)# 第二项用合并后的实际簇数 k2;加 1e-8 防止 SSE=0 时 log(0) bic = n * math.log((sse + 1e-8) / n) + k2 * (d + 1) * math.log(n)# 严格小于,保证 BIC 并列时取较小的 Kif bic < best_bic: best_bic = bic best = (c2, l2, k2) c2, l2, k2 = best out = [str(k2)]for c in c2: out.append(" ".join(fmt(v) for v in c)) out.append(" ".join(str(x) for x in l2))return "\n".join(out)def main():# 读入点数、维度、坐标以及簇数区间 n, d = map(int, input().split()) pts = []for _ in range(n): pts.append([float(x) for x in input().split()]) kmin, kmax = map(int, input().split())print(solve(pts, n, d, kmin, kmax))if __name__ == "__main__": main()
刷题练习:CodeFun2000.com















































































