转载自nju_spy大佬的博客,记录了华为AI岗位笔试的题目与解答。
一、华为代码题
1. DBSCAN聚类 (10.10)
题目描述: 使用DBSCAN算法在二维或三维实数坐标上进行聚类,输出簇的数量和噪声点的数量。
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法,它不需要预先指定簇的数量,并且能够发现任意形状的簇,同时识别噪声点。
核心思想: 如果一个点的邻域内(半径 eps)包含足够多的点(min_samples),则该点被视为核心点,并以此扩展形成一个簇。
完整代码实现:
import math
from collections import deque
def euclidean_distance(p1, p2):
"""计算两点之间的欧氏距离"""
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p1, p2)))
def dbscan(points, eps, min_samples):
"""
DBSCAN聚类算法
Args:
points: 数据点列表,每个点是一个坐标元组
eps: 邻域半径
min_samples: 核心点的最小邻居数
Returns:
labels: 每个点的聚类标签,-1表示噪声点
n_clusters: 簇的数量
n_noise: 噪声点的数量
"""
n = len(points)
labels = [0] * n # 0: 未访问, -1: 噪声, >0: 簇编号
cluster_id = 0
def region_query(point_idx):
"""查找点point_idx的eps邻域内所有点的索引"""
neighbors = []
for i in range(n):
if euclidean_distance(points[point_idx], points[i]) <= eps:
neighbors.append(i)
return neighbors
def expand_cluster(point_idx, neighbors, cluster_id):
"""从核心点扩展簇"""
labels[point_idx] = cluster_id
queue = deque(neighbors)
while queue:
current = queue.popleft()
if labels[current] == -1:
# 将噪声点重新分配到当前簇(边界点)
labels[current] = cluster_id
elif labels[current] == 0:
# 未访问的点
labels[current] = cluster_id
current_neighbors = region_query(current)
if len(current_neighbors) >= min_samples:
# 当前点也是核心点,将其邻居加入队列继续扩展
queue.extend(current_neighbors)
# 主循环
for i in range(n):
if labels[i] != 0:
continue # 已经访问过
neighbors = region_query(i)
if len(neighbors) < min_samples:
labels[i] = -1 # 标记为噪声点
else:
cluster_id += 1
expand_cluster(i, neighbors, cluster_id)
n_clusters = cluster_id
n_noise = sum(1 for label in labels if label == -1)
return labels, n_clusters, n_noise
def main():
"""解析输入并执行DBSCAN"""
# 输入格式:
# 第一行:n eps min_samples
# 接下来n行:每行的坐标(空格分隔)
n, eps, min_samples = map(float, input().split())
n = int(n)
points = []
for _ in range(n):
coord = tuple(map(float, input().split()))
points.append(coord)
labels, n_clusters, n_noise = dbscan(points, eps, min_samples)
print(f"簇的数量:{n_clusters}")
print(f"噪声点数量:{n_noise}")
# 打印每个点的标签
for i, label in enumerate(labels):
if label == -1:
print(f"点{i}: 噪声")
else:
print(f"点{i}: 簇{label}")
if __name__ == "__main__":
main()
算法复杂度分析:
- 时间复杂度:O(n^2),主要消耗在区域查询
- 空间复杂度:O(n)
使用KD-Tree或Ball-Tree可以将区域查询优化到O(n log n)。
2. 实现Masked Multi-Head Self-Attention (9.28)
题目描述: 给定批量序列表示 X 与权重矩阵 W_Q, W_K, W_V, W_O,实现带有因果掩码的Multi-Head Self-Attention。
这是Transformer架构的核心组件。Masked Self-Attention保证每个位置只能注意到它自身及之前的位置(Causal Mask),在GPT等自回归语言模型中至关重要。
完整代码实现:
import numpy as np
def softmax(x, axis=-1):
"""
数值稳定的Softmax函数
Args:
x: 输入数组
axis: 沿哪个轴计算softmax
Returns:
softmax概率分布
"""
# 减去最大值以防止数值溢出
x_max = np.max(x, axis=axis, keepdims=True)
exp_x = np.exp(x - x_max)
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
def masked_multi_head_attention(X, W_Q, W_K, W_V, W_O, mask=None):
"""
Masked Multi-Head Self-Attention
Args:
X: 输入序列 (batch_size, seq_len, d_model)
W_Q: Query权重 (num_heads, d_model, d_k)
W_K: Key权重 (num_heads, d_model, d_k)
W_V: Value权重 (num_heads, d_model, d_v)
W_O: Output投影权重 (num_heads * d_v, d_model)
mask: 注意力掩码 (seq_len, seq_len),默认使用因果掩码
Returns:
output: 注意力输出 (batch_size, seq_len, d_model)
"""
batch_size, seq_len, d_model = X.shape
num_heads = W_Q.shape[0]
d_k = W_Q.shape[2]
d_v = W_V.shape[2]
# 1. 生成因果掩码(Causal Mask / Upper Triangular Mask)
if mask is None:
# 下三角矩阵:可以注意到自身及之前的位置
mask = np.triu(np.ones((seq_len, seq_len)), k=1) * -1e9
# 0的地方(下三角)可以注意,1的地方(上三角)被mask掉
# 使用极大的负数作为mask值,这样经过softmax后概率趋近于0
# 2. 计算Q, K, V — 为每个头独立线性变换
# X: (B, L, D) → Q: (B, H, L, d_k)
Q = np.zeros((batch_size, num_heads, seq_len, d_k))
K = np.zeros((batch_size, num_heads, seq_len, d_k))
V = np.zeros((batch_size, num_heads, seq_len, d_v))
for h in range(num_heads):
# 每个头使用自己的权重矩阵
for b in range(batch_size):
Q[b, h] = X[b] @ W_Q[h] # (L, d_model) @ (d_model, d_k) → (L, d_k)
K[b, h] = X[b] @ W_K[h] # (L, d_model) @ (d_model, d_k) → (L, d_k)
V[b, h] = X[b] @ W_V[h] # (L, d_model) @ (d_model, d_v) → (L, d_v)
# 3. 计算Scaled Dot-Product Attention
# Attention(Q, K, V) = softmax(QK^T / sqrt(d_k) + mask) * V
scale = np.sqrt(d_k)
# QK^T: (B, H, L, L)
scores = Q @ K.transpose(0, 1, 3, 2) / scale # (B, H, L, d_k) @ (B, H, d_k, L) → (B, H, L, L)
# 添加mask:将不应注意的位置设为极小值
scores = scores + mask # mask广播到 (B, H, L, L)
# Softmax沿最后一个维度(每个查询对所有键的注意力权重)
attn_weights = softmax(scores, axis=-1) # (B, H, L, L)
# 加权求和:attn @ V
context = attn_weights @ V # (B, H, L, L) @ (B, H, L, d_v) → (B, H, L, d_v)
# 4. 拼接多头并做输出投影
# 将 (B, H, L, d_v) reshape 为 (B, L, H * d_v)
concat = np.zeros((batch_size, seq_len, num_heads * d_v))
for b in range(batch_size):
concat[b] = context[b].transpose(1, 0, 2).reshape(seq_len, num_heads * d_v)
# 输出投影: (B, L, H*d_v) @ (H*d_v, d_model) → (B, L, d_model)
output = concat @ W_O
return output
def main():
"""解析输入并执行Masked Multi-Head Attention"""
# 输入格式(示例):
# 第一行:batch_size seq_len d_model num_heads d_k d_v
# 然后是X矩阵,然后是W_Q, W_K, W_V, W_O矩阵
data = input().strip().split()
batch_size = int(data[0])
seq_len = int(data[1])
d_model = int(data[2])
num_heads = int(data[3])
d_k = int(data[4])
d_v = int(data[5])
# 读取输入矩阵(简化起见,此处用随机数据演示)
# 实际笔试中会按照给定格式逐行读取
np.random.seed(42)
X = np.random.randn(batch_size, seq_len, d_model)
W_Q = np.random.randn(num_heads, d_model, d_k)
W_K = np.random.randn(num_heads, d_model, d_k)
W_V = np.random.randn(num_heads, d_model, d_v)
W_O = np.random.randn(num_heads * d_v, d_model)
output = masked_multi_head_attention(X, W_Q, W_K, W_V, W_O)
print(f"输出形状:{output.shape}") # (batch_size, seq_len, d_model)
print("前3个位置的输出值:")
print(output[0, :3, :3])
if __name__ == "__main__":
main()
关键点总结:
| 概念 | 说明 |
|---|---|
| Self-Attention | Q、K、V都来自同一个输入X |
| Multi-Head | 多个注意力头并行计算,每个头关注不同的表示子空间 |
| Scaled Dot-Product | 除以sqrt(d_k)防止点积过大导致softmax梯度消失 |
| Causal Mask | 上三角掩码,保证自回归生成时不会"偷看"未来信息 |
| 输出投影 | 拼接所有头后通过W_O变换回d_model维度 |
二、代码题读入格式
笔试中常见的输入格式及处理方式:
单行读入一个整数
n = int(input())
单行读入多个整数
a, b, c = map(int, input().split())
读入多行字符串
lines = []
while True:
try:
line = input().strip()
if not line:
break
lines.append(line)
except EOFError:
break
读入二维矩阵
n, m = map(int, input().split())
matrix = []
for _ in range(n):
row = list(map(int, input().split()))
matrix.append(row)
使用eval读取列表格式
# 输入:[[1,2],[3,4],[5,6]]
arr = eval(input()) # 直接将字符串解析为Python列表
格式化输出小数
# 保留两位小数
print(f"{value:.2f}")
# 或
print("{:.2f}".format(value))
三、选择题
1. LLM三阶段训练中SFT的目标是什么?
A. 让模型学会生成多样化的文本 B. 让模型学会遵循指令 C. 让模型与人类偏好对齐 D. 让模型学会知识推理
答案:B
解析: LLM三阶段训练流程是:Pre-training(预训练)→ SFT(Supervised Fine-Tuning,有监督微调)→ RLHF(基于人类反馈的强化学习)。SFT阶段使用指令-回答对进行训练,目的是让模型学会理解和遵循人类指令。预训练阶段学习通用知识,RLHF阶段与人类偏好对齐。
2. (题目缺失,跳过)
3. 输入224×224×3的图片,经过一个7×7卷积(stride=2, padding=3),再经过一个3×3最大池化(stride=2, padding=1),输出特征图的尺寸是多少?
答案:112 × 112
解析:
卷积输出尺寸公式:O = floor((I - K + 2P) / S) + 1
第一层卷积:
- O = floor((224 - 7 + 2×3) / 2) + 1 = floor(223/2) + 1 = 111 + 1 = 112
- 输出:112 × 112
第二层池化:
- O = floor((112 - 3 + 2×1) / 2) + 1 = floor(111/2) + 1 = 55 + 1 = 56
- 输出:56 × 56
4. 掷硬币期望问题
抛一枚均匀硬币,求连续出现两次正面的期望投掷次数。
答案:6次
解析: 这是一个经典的马尔可夫链问题。设E0为当前没有正面(状态0)时还需投掷的期望次数,E1为当前已有一个正面(状态1)时还需投掷的期望次数。
状态转移:
- 在状态0:以1/2概率进入状态1(投出正面),以1/2概率留在状态0(投出反面)
- 在状态1:以1/2概率结束(再投出正面),以1/2概率回到状态0(投出反面)
方程:
- E0 = 1 + 1/2 × E1 + 1/2 × E0
- E1 = 1 + 1/2 × 0 + 1/2 × E0
解得:E0 = 6, E1 = 4
5. Stacking集成学习中第二层模型的作用是什么?
答案: 学习如何组合第一层基模型的预测结果。
解析: Stacking(堆叠泛化)是一种集成学习方法。第一层由多个不同的基学习器组成,每个基学习器对训练数据进行预测。第二层(元学习器)以第一层模型的预测结果作为输入特征,学习如何最优地组合这些预测。这类似于让一个"裁判"模型来学习各个"专家"模型的投票权重。
6. Tokenizer与Embedding对比表
| 特性 | Tokenizer | Embedding |
|---|---|---|
| 功能 | 将文本切分为token序列 | 将token映射为稠密向量 |
| 输出 | 整数ID序列 | 浮点数向量 |
| 维度 | 1维(token_id) | d维向量(如768) |
| 是否可训练 | BPE等算法不可训练 | 可训练(神经网络层) |
| 代表 | BPE, WordPiece, SentencePiece | nn.Embedding |
Tokenizer将"你好世界" → [101, 872, 1962, 686, 102]
Embedding将[101, 872, …] → [[0.12, -0.34, …], [0.56, 0.78, …], …]
关键区别: Tokenizer负责分词(离散化),Embedding负责向量化(连续化)。两者是NLP流程中相邻但功能完全不同的步骤。
7. 欠定线性方程组(方程数少于未知数)的最小二范数解由什么给出?
答案: 伪逆(Moore-Penrose pseudoinverse)
解析: 对于欠定方程组 Ax = b(其中A的行数小于列数),存在无穷多解。其中二范数最小的解是 x = A^T (AA^T)^{-1} b,这实际上就是A的伪逆乘以b。这个解的几何意义是:在所有满足Ax=b的解中,距离原点最近的那个。这也称为最小范数解(minimum norm solution)。
8. 下列哪个激活函数有助于缓解梯度消失问题?
A. Sigmoid B. Tanh C. Softmax D. ReLU
答案:D
解析:
- Sigmoid: 导数最大值为0.25,多层网络中梯度会指数级衰减,容易造成梯度消失。
- Tanh: 导数最大值为1,比Sigmoid好但深层网络仍会梯度消失。
- Softmax: 用于分类输出的归一化,不是隐藏层激活函数。
- ReLU: 正区间导数为1(恒定),不会衰减梯度,有效缓解梯度消失。但需要注意"Dead ReLU"问题(负区间导数为0)。
9. 混合精度训练中,为什么需要对损失进行缩放(Loss Scaling)?
A. 加快训练速度 B. 防止梯度下溢 C. 提高模型精度 D. 减少内存占用
答案:B
解析: 混合精度训练使用FP16进行前向和反向传播,但FP16的数值范围有限(最小正数约6×10^-8)。当梯度值非常小时,在FP16下会变成0(下溢),导致参数无法更新。Loss Scaling的做法是将loss乘以一个较大的系数(如2^16),使反向传播的梯度的值变大,在更新参数前再除以相同系数还原。这样就保护了小子度不会在FP16中消失。
10. 8张NVIDIA A100-80GB,使用模型并行训练一个含175B参数的大模型……
涉及计算:
- 模型参数:175B,FP16下为 175B × 2 bytes = 350 GB
- 优化器状态(Adam):需要存储参数、动量、方差,共3份 → 350 × 3 = 1050 GB
- 梯度:350 GB(与参数量相同)
- 总计:350 + 1050 + 350 = 1750 GB
- 8张A100-80GB总显存:8 × 80 = 640 GB
模型并行+梯度检查点+ZeRO优化可以将显存需求降到可用范围内。
11. 关于Causal Mask的正确说法
答案:A, B
选项分析:
- A(正确):Causal Mask是上三角矩阵(或下三角,取决于实现),确保位置i只能注意位置1到i。
- B(正确):掩码位置通常填充
-inf(或极大的负数),经过softmax后注意力权重趋近于0。 - C:Causal Mask不是随机掩码,而是确定的三角结构。
- D:Causal Mask在训练时也使用(Teacher Forcing),推理时逐token生成也需要。
小结
本次笔试涵盖了三个主要方向:
- 算法实现能力:通过DBSCAN聚类和Multi-Head Attention的代码题,考察对经典算法细节的掌握和动手编码能力
- 输入输出处理:理解不同格式的数据输入方式,是顺利通过笔试的基础
- 理论基础:深度学习、机器学习基础知识,包括但不限于Transformer、模型训练、集成学习、线性代数、优化技巧等
后续准备建议:重点复习Transformer架构细节、常见聚类/分类算法的手动实现、PyTorch/NumPy操作的熟练度,以及大模型训练相关的工程知识。
