AI面试代码题常考的10个算法:从梯度下降到注意力实现
AI面试10大常考代码题详解:线性回归、逻辑回归、K-Means、KNN、决策树分裂、BP反向传播、Self-Attention、Layer Norm、Beam Search、RAG流程,每题含考察点+代码框架+注意事项
背景介绍
AI面试的代码题和普通算法题不一样,很多是让你手写ML/DL的核心算法。我自己在秋招中被要求手写过线性回归、Self-Attention、K-Means等,一开始写得很磕绊,后来专门花了一周时间集中练习,把常考的10个算法都练到了肌肉记忆。今天把这10个常考算法整理出来,每个包含考察点、代码框架和注意事项。
本文所有代码用Python/NumPy实现,不依赖深度学习框架。面试中一般也要求用NumPy手写,不用PyTorch。
1. 手写线性回归(梯度下降)
考察点:梯度下降原理、损失函数推导、学习率选择、特征归一化
代码框架:
class LinearRegression:
def __init__(self, lr=0.01, n_iters=1000):
self.lr = lr
self.n_iters = n_iters
def fit(self, X, y):
n_samples, n_features = X.shape
self.w = np.zeros(n_features)
self.b = 0
for _ in range(self.n_iters):
y_pred = X @ self.w + self.b
dw = (2 / n_samples) * (X.T @ (y_pred - y))
db = (2 / n_samples) * np.sum(y_pred - y)
self.w -= self.lr * dw
self.b -= self.lr * db
def predict(self, X):
return X @ self.w + self.b
注意事项:①特征要归一化,否则梯度下降收敛很慢;②学习率太大不收敛,太小收敛慢,可以画loss曲线调试;③面试官可能追问闭式解(w = (X^T X)^{-1} X^T y),要知道什么情况下闭式解不可用(X^T X不可逆、数据量太大)。
2. 手写逻辑回归
考察点:Sigmoid函数、交叉熵损失、梯度计算、多分类扩展
代码框架:
class LogisticRegression:
def __init__(self, lr=0.01, n_iters=1000):
self.lr = lr
self.n_iters = n_iters
def sigmoid(self, z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
def fit(self, X, y):
n_samples, n_features = X.shape
self.w = np.zeros(n_features)
self.b = 0
for _ in range(self.n_iters):
z = X @ self.w + self.b
y_pred = self.sigmoid(z)
dw = (1 / n_samples) * (X.T @ (y_pred - y))
db = (1 / n_samples) * np.sum(y_pred - y)
self.w -= self.lr * dw
self.b -= self.lr * db
def predict(self, X):
return (self.sigmoid(X @ self.w + self.b) >= 0.5).astype(int)
注意事项:①Sigmoid要clip防止数值溢出;②交叉熵损失的梯度推导要会手推;③面试官可能问多分类怎么做(Softmax回归)。
3. 手写K-Means
考察点:聚类原理、初始化策略、收敛判断、K值选择
代码框架:
class KMeans:
def __init__(self, k=3, max_iters=100):
self.k = k
self.max_iters = max_iters
def fit(self, X):
idx = np.random.choice(len(X), self.k, replace=False)
self.centroids = X[idx]
for _ in range(self.max_iters):
distances = np.linalg.norm(X[:, None] - self.centroids[None, :], axis=2)
labels = np.argmin(distances, axis=1)
new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(self.k)])
if np.allclose(self.centroids, new_centroids):
break
self.centroids = new_centroids
self.labels = labels
注意事项:①随机初始化可能导致局部最优,面试官会问K-Means++初始化;②空簇怎么处理(重新随机初始化);③K值选择:肘部法则、轮廓系数。
4. 手写KNN
考察点:距离度量、K值选择、投票策略、时间复杂度
代码框架:
class KNN:
def __init__(self, k=3):
self.k = k
def fit(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X):
predictions = []
for x in X:
distances = np.linalg.norm(self.X_train - x, axis=1)
k_indices = np.argsort(distances)[:self.k]
k_labels = self.y_train[k_indices]
predictions.append(np.bincount(k_labels).argmax())
return np.array(predictions)
注意事项:①预测时间复杂度O(ndk),n是训练样本数,d是维度;②面试官可能问KD树加速;③K太小过拟合,K太大欠拟合;④距离加权KNN(距离越近权重越大)。
5. 手写决策树分裂
考察点:信息增益/信息增益率/基尼指数、分裂策略、连续值处理
代码框架(以基尼指数为例):
def gini(y):
classes, counts = np.unique(y, return_counts=True)
probs = counts / len(y)
return 1 - np.sum(probs ** 2)
def best_split(X, y, feature_idx):
values = np.sort(np.unique(X[:, feature_idx]))
best_gini, best_threshold = 1, None
for i in range(len(values) - 1):
threshold = (values[i] + values[i + 1]) / 2
left_mask = X[:, feature_idx] <= threshold
gini_split = (left_mask.sum() * gini(y[left_mask]) + (~left_mask).sum() * gini(y[~left_mask])) / len(y)
if gini_split < best_gini:
best_gini, best_threshold = gini_split, threshold
return best_threshold, best_gini
注意事项:①ID3用信息增益,C4.5用信息增益率,CART用基尼指数;②信息增益偏向多值特征,增益率做了校正;③连续值特征要排序后遍历候选分裂点。
6. 手写BP反向传播
考察点:前向传播、损失计算、反向传播、梯度更新
代码框架(单隐层网络):
class NeuralNetwork:
def __init__(self, input_size, hidden_size, output_size, lr=0.01):
self.W1 = np.random.randn(input_size, hidden_size) * 0.01
self.b1 = np.zeros(hidden_size)
self.W2 = np.random.randn(hidden_size, output_size) * 0.01
self.b2 = np.zeros(output_size)
self.lr = lr
def relu(self, z):
return np.maximum(0, z)
def relu_grad(self, z):
return (z > 0).astype(float)
def forward(self, X):
self.z1 = X @ self.W1 + self.b1
self.a1 = self.relu(self.z1)
self.z2 = self.a1 @ self.W2 + self.b2
return self.z2
def backward(self, X, y, y_pred):
m = X.shape[0]
dz2 = (y_pred - y) / m
dW2 = self.a1.T @ dz2
db2 = np.sum(dz2, axis=0)
da1 = dz2 @ self.W2.T
dz1 = da1 * self.relu_grad(self.z1)
dW1 = X.T @ dz1
db1 = np.sum(dz1, axis=0)
self.W2 -= self.lr * dW2
self.b2 -= self.lr * db2
self.W1 -= self.lr * dW1
self.b1 -= self.lr * db1
注意事项:①权重初始化用小随机值,不能用零初始化(对称性问题);②梯度要除以batch size;③面试官可能追问Vanishing Gradient和如何解决。
7. 手写Self-Attention
考察点:QKV计算、缩放点积注意力、多头注意力、Mask
代码框架:
def self_attention(X, W_q, W_k, W_v, mask=None):
Q = X @ W_q
K = X @ W_k
V = X @ W_v
d_k = Q.shape[-1]
scores = Q @ K.T / np.sqrt(d_k)
if mask is not None:
scores = np.where(mask == 0, -1e9, scores)
attn_weights = np.exp(scores - scores.max(axis=-1, keepdims=True))
attn_weights = attn_weights / attn_weights.sum(axis=-1, keepdims=True)
output = attn_weights @ V
return output, attn_weights
注意事项:①除以sqrt(d_k)防止点积过大导致Softmax梯度消失;②Softmax数值稳定(减最大值);③Mask用于Decoder防止看到未来信息;④多头注意力是多个Self-Attention拼接后线性变换。
8. 手写Layer Norm
考察点:归一化原理、与Batch Norm的区别、为什么Transformer用Layer Norm
代码框架:
def layer_norm(x, gamma=1.0, beta=0.0, eps=1e-5):
mean = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
x_norm = (x - mean) / np.sqrt(var + eps)
return gamma * x_norm + beta
注意事项:①Layer Norm沿特征维度归一化,Batch Norm沿batch维度归一化;②Transformer用Layer Norm因为序列长度可变,Batch Norm效果不稳定;③gamma和beta是可学习参数;④RMSNorm是Layer Norm的简化版(不减均值)。
9. 手写Beam Search
考察点:解码策略、Beam Size选择、与贪心搜索/采样的区别
代码框架:
def beam_search(model_init_state, decode_step, beam_width, max_len, start_token, end_token):
sequences = [[[], 0.0, model_init_state]]
for _ in range(max_len):
all_candidates = []
for seq, score, state in sequences:
if seq and seq[-1] == end_token:
all_candidates.append((seq, score, state))
continue
logits, new_state = decode_step(state, seq[-1] if seq else start_token)
log_probs = logits - np.max(logits)
log_probs = log_probs - np.log(np.sum(np.exp(log_probs)))
top_indices = np.argsort(log_probs)[-beam_width:]
for idx in top_indices:
all_candidates.append((seq + [idx], score + log_probs[idx], new_state))
sequences = sorted(all_candidates, key=lambda x: x[1], reverse=True)[:beam_width]
return sequences[0][0]
注意事项:①用log概率相加代替概率相乘,防止数值下溢;②Beam Size越大搜索越全但越慢;③长度惩罚(length penalty)防止偏向短序列;④面试官可能问对比Sampling、Top-k、Top-p等解码策略。
10. 手写简单的RAG流程
考察点:文档切分、向量化、相似度检索、Prompt构建
代码框架:
def simple_rag(query, documents, embed_fn, generate_fn, top_k=3):
chunks = []
for doc in documents:
words = doc.split()
for i in range(0, len(words), 200):
chunk = " ".join(words[i:i+200])
chunks.append(chunk)
chunk_embeddings = np.array([embed_fn(c) for c in chunks])
query_embedding = embed_fn(query)
scores = chunk_embeddings @ query_embedding
top_indices = np.argsort(scores)[-top_k:][::-1]
retrieved = [chunks[i] for i in top_indices]
context = "\n".join(retrieved)
prompt = f"Based on the following context:\n{context}\n\nQuestion: {query}\nAnswer:"
answer = generate_fn(prompt)
return answer, retrieved
注意事项:①文档切分策略(固定长度 vs 语义切分 vs 递归切分);②检索用余弦相似度(先归一化再点积);③面试官可能问混合检索(向量+关键词)、重排序(Reranker)、以及如何处理检索不到相关文档的情况。
真题汇总
1. 手写线性回归(梯度下降)
2. 手写逻辑回归
3. 手写K-Means
4. 手写KNN
5. 手写决策树分裂
6. 手写BP反向传播
7. 手写Self-Attention
8. 手写Layer Norm
9. 手写Beam Search
10. 手写简单的RAG流程
心得建议
1. 每个算法至少手写3遍:第一遍照着写,第二遍脱稿写,第三遍限时写(30分钟内)。只有练到肌肉记忆,面试时才不会卡壳。
2. 注意数值稳定性:Softmax减最大值、Sigmoid做clip、log概率代替概率相乘,这些tricks面试官很看重。
3. 理解比记忆更重要:不要死记代码,而是理解每一步为什么这么做。面试官可能会让你修改代码(比如把MSE换成MAE),理解了才能灵活应对。
4. 矩阵运算要熟练:AI代码题大量使用矩阵运算,NumPy的广播机制、维度变换要非常熟练。建议在纸上先画维度,确认无误再写代码。
5. 边写边讲:面试中不要闷头写代码,边写边解释你的思路。面试官更看重你的思考过程,而不是最终代码是否完美。
FAQ
Q:面试中用什么语言写?
A:大部分公司接受Python,部分公司要求C++。建议Python为主,NumPy手写。
Q:需要跑通吗?
A:能跑通最好,但面试官更看重逻辑正确性。如果时间不够,写出核心逻辑即可。
Q:可以用PyTorch吗?
A:一般不允许。面试官想看你对底层运算的理解,用PyTorch一行nn.Linear()就没了。
Q:时间不够写不完怎么办?
A:先写核心逻辑(比如Self-Attention的QKV计算和注意力权重),辅助部分(Mask、Dropout)口头说明即可。
Q:除了这10个还有哪些常考的?
A:NMS、IoU、卷积操作、池化操作、Focal Loss、交叉熵损失。建议也练一下。