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