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、交叉熵損失。建議也練一下。