AI面试代码题常考的10个算法:从梯度下降到注意力实现

AI代码题作者: 美历团队

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、交叉熵损失。建议也练一下。

#代码题#手写算法#梯度下降#Self-Attention#K-Means#RAG#Beam Search