Python实现5大基础机器学习算法:从零理解核心原理
1. 为什么需要从零实现机器学习算法?
在机器学习领域,调用现成的库(如scikit-learn)固然方便,但真正理解算法本质的唯一途径就是亲手实现它们。我仍然记得第一次用Python实现线性回归时的顿悟时刻——那些在教科书上看不懂的数学公式,在代码中突然变得清晰可见。
从零实现机器学习算法能带来三个核心价值:
- 深入理解算法内部的数学原理和工作机制
- 获得定制和优化算法的能力,不再受限于现有库的功能
- 培养解决实际问题的工程化思维,而不仅仅是调参技巧
本文将以Python为例,带你实现5个最基础的机器学习算法:线性回归、逻辑回归、KNN、朴素贝叶斯和决策树。每个实现都将控制在100行代码以内,但会包含完整的训练和预测流程。
2. 准备工作与环境搭建
2.1 最小化工具链配置
我们只需要最基础的Python科学计算环境:
pip install numpy matplotlib
注意:刻意不使用任何机器学习专用库,甚至连scipy都不需要。这是为了确保我们真的从最底层理解每个算法的实现。
2.2 测试数据生成
为了验证算法效果,我们先实现一个简单的数据生成器:
import numpy as np
def make_regression_data(n_samples=100, noise=0.1):
X = np.random.rand(n_samples, 1) * 2 - 1
y = 5 * X + 3 + np.random.normal(0, noise, (n_samples, 1))
return X, y
def make_classification_data(n_samples=100):
X = np.random.randn(n_samples, 2)
y = (X[:, 0] > 0).astype(int)
return X, y
3. 线性回归实现
3.1 数学原理回顾
线性回归的核心是最小二乘法,目标是最小化损失函数: [ J(\theta) = \frac{1}{2m}\sum_{i=1}^m(h_\theta(x^{(i)}) - y^{(i)})^2 ] 其中 ( h_\theta(x) = \theta^Tx )
3.2 梯度下降实现
class LinearRegression:
def __init__(self, lr=0.01, n_iters=1000):
self.lr = lr
self.n_iters = n_iters
self.weights = None
self.bias = None
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
for _ in range(self.n_iters):
y_pred = np.dot(X, self.weights) + self.bias
dw = (1/n_samples) * np.dot(X.T, (y_pred - y))
db = (1/n_samples) * np.sum(y_pred - y)
self.weights -= self.lr * dw
self.bias -= self.lr * db
def predict(self, X):
return np.dot(X, self.weights) + self.bias
实操技巧:学习率(lr)通常从0.01开始尝试,如果损失震荡不收敛就调小,如果下降太慢就调大。
4. 逻辑回归实现
4.1 Sigmoid函数与决策边界
逻辑回归使用sigmoid函数将线性输出映射到(0,1)区间: [ \sigma(z) = \frac{1}{1+e^{-z}} ]
def sigmoid(x):
return 1 / (1 + np.exp(-x))
class LogisticRegression:
def __init__(self, lr=0.01, n_iters=1000):
self.lr = lr
self.n_iters = n_iters
self.weights = None
self.bias = None
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0
for _ in range(self.n_iters):
linear = np.dot(X, self.weights) + self.bias
y_pred = sigmoid(linear)
dw = (1/n_samples) * np.dot(X.T, (y_pred - y))
db = (1/n_samples) * np.sum(y_pred - y)
self.weights -= self.lr * dw
self.bias -= self.lr * db
def predict(self, X):
linear = np.dot(X, self.weights) + self.bias
y_pred = sigmoid(linear)
return [1 if i > 0.5 else 0 for i in y_pred]
4.2 多分类扩展技巧
通过One-vs-Rest策略可以实现多分类:
class MulticlassLogisticRegression:
def __init__(self, n_classes, lr=0.01, n_iters=1000):
self.n_classes = n_classes
self.classifiers = [LogisticRegression(lr, n_iters)
for _ in range(n_classes)]
def fit(self, X, y):
for i in range(self.n_classes):
y_binary = (y == i).astype(int)
self.classifiers[i].fit(X, y_binary)
def predict(self, X):
probs = np.array([clf.predict_proba(X)
for clf in self.classifiers])
return np.argmax(probs, axis=0)
5. K近邻算法实现
5.1 距离度量选择
KNN的核心是距离计算,常用欧式距离: [ d(x, y) = \sqrt{\sum_{i=1}^n(x_i - y_i)^2} ]
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):
y_pred = [self._predict(x) for x in X]
return np.array(y_pred)
def _predict(self, x):
distances = [np.sqrt(np.sum((x - x_train)**2))
for x_train in self.X_train]
k_indices = np.argsort(distances)[:self.k]
k_labels = [self.y_train[i] for i in k_indices]
return max(set(k_labels), key=k_labels.count)
5.2 效率优化技巧
原始KNN的时间复杂度是O(n),可以通过KD树优化到O(log n):
from collections import Counter
class KNN_KD:
def __init__(self, k=3):
self.k = k
self.tree = None
def fit(self, X, y):
self.tree = KDTree(X)
self.y_train = y
def predict(self, X):
dist, ind = self.tree.query(X, k=self.k)
k_labels = [self.y_train[i] for i in ind]
return [Counter(labels).most_common(1)[0][0]
for labels in k_labels]
6. 朴素贝叶斯实现
6.1 概率计算原理
基于贝叶斯定理: [ P(y|x) = \frac{P(x|y)P(y)}{P(x)} ] "朴素"假设特征条件独立: [ P(x|y) = \prod_{i=1}^n P(x_i|y) ]
class NaiveBayes:
def fit(self, X, y):
n_samples, n_features = X.shape
self.classes = np.unique(y)
n_classes = len(self.classes)
self.mean = np.zeros((n_classes, n_features))
self.var = np.zeros((n_classes, n_features))
self.priors = np.zeros(n_classes)
for c in self.classes:
X_c = X[y == c]
self.mean[c] = X_c.mean(axis=0)
self.var[c] = X_c.var(axis=0)
self.priors[c] = X_c.shape[0] / n_samples
def predict(self, X):
y_pred = [self._predict(x) for x in X]
return np.array(y_pred)
def _predict(self, x):
posteriors = []
for c in self.classes:
prior = np.log(self.priors[c])
likelihood = np.sum(np.log(self._pdf(c, x)))
posterior = prior + likelihood
posteriors.append(posterior)
return self.classes[np.argmax(posteriors)]
def _pdf(self, class_idx, x):
mean = self.mean[class_idx]
var = self.var[class_idx]
numerator = np.exp(-(x - mean)**2 / (2 * var))
denominator = np.sqrt(2 * np.pi * var)
return numerator / denominator
注意:实际计算中使用对数概率避免下溢问题,同时高斯分布假设可能不适合所有特征。
7. 决策树实现
7.1 信息增益计算
使用信息熵作为划分标准: [ \text{Entropy} = -\sum_{i=1}^c p_i \log_2 p_i ] 信息增益: [ \text{IG} = \text{Entropy(parent)} - \sum \frac{N_v}{N} \text{Entropy(child}_v) ]
class DecisionTree:
def __init__(self, max_depth=5):
self.max_depth = max_depth
def fit(self, X, y):
self.n_classes = len(np.unique(y))
self.n_features = X.shape[1]
self.tree = self._grow_tree(X, y)
def _grow_tree(self, X, y, depth=0):
n_samples, n_features = X.shape
n_labels = len(np.unique(y))
if (depth >= self.max_depth or n_labels == 1):
leaf_value = self._most_common_label(y)
return {'value': leaf_value}
feat_idxs = np.random.choice(n_features,
int(np.sqrt(n_features)),
replace=False)
best_feat, best_thresh = self._best_split(X, y, feat_idxs)
left_idxs, right_idxs = self._split(X[:, best_feat], best_thresh)
left = self._grow_tree(X[left_idxs, :], y[left_idxs], depth+1)
right = self._grow_tree(X[right_idxs, :], y[right_idxs], depth+1)
return {'feature': best_feat,
'threshold': best_thresh,
'left': left,
'right': right}
def _best_split(self, X, y, feat_idxs):
best_gain = -1
split_idx, split_thresh = None, None
for feat_idx in feat_idxs:
X_column = X[:, feat_idx]
thresholds = np.unique(X_column)
for thresh in thresholds:
gain = self._information_gain(y, X_column, thresh)
if gain > best_gain:
best_gain = gain
split_idx = feat_idx
split_thresh = thresh
return split_idx, split_thresh
def _information_gain(self, y, X_column, threshold):
parent_entropy = self._entropy(y)
left_idxs, right_idxs = self._split(X_column, threshold)
if len(left_idxs) == 0 or len(right_idxs) == 0:
return 0
n = len(y)
n_l, n_r = len(left_idxs), len(right_idxs)
e_l, e_r = self._entropy(y[left_idxs]), self._entropy(y[right_idxs])
child_entropy = (n_l/n) * e_l + (n_r/n) * e_r
return parent_entropy - child_entropy
def _split(self, X_column, threshold):
left_idxs = np.argwhere(X_column <= threshold).flatten()
right_idxs = np.argwhere(X_column > threshold).flatten()
return left_idxs, right_idxs
def _entropy(self, y):
hist = np.bincount(y)
ps = hist / len(y)
return -np.sum([p * np.log2(p) for p in ps if p > 0])
def _most_common_label(self, y):
return np.argmax(np.bincount(y))
def predict(self, X):
return np.array([self._traverse_tree(x, self.tree) for x in X])
def _traverse_tree(self, x, node):
if 'value' in node:
return node['value']
if x[node['feature']] <= node['threshold']:
return self._traverse_tree(x, node['left'])
return self._traverse_tree(x, node['right'])
7.2 预剪枝与特征采样
实现中已经包含两个关键优化:
- 最大深度限制(max_depth)
- 特征随机采样(np.sqrt(n_features))
8. 算法测试与比较
8.1 回归任务测试
X_reg, y_reg = make_regression_data()
lr = LinearRegression()
lr.fit(X_reg, y_reg)
predictions = lr.predict(X_reg)
plt.scatter(X_reg, y_reg)
plt.plot(X_reg, predictions, color='red')
plt.show()
8.2 分类任务测试
X_clf, y_clf = make_classification_data()
models = {
"Logistic Regression": LogisticRegression(),
"KNN": KNN(k=3),
"Naive Bayes": NaiveBayes(),
"Decision Tree": DecisionTree(max_depth=5)
}
for name, model in models.items():
model.fit(X_clf, y_clf)
accuracy = np.mean(model.predict(X_clf) == y_clf)
print(f"{name} Accuracy: {accuracy:.2f}")
9. 工程实践中的注意事项
-
数值稳定性 :
- 概率计算使用对数空间
- 添加极小值(epsilon)防止除零错误
- 特征缩放(标准化)提升收敛速度
-
效率优化 :
- 使用向量化操作替代循环
- 对KNN等算法使用近似最近邻搜索
- 对决策树进行后剪枝
-
扩展性考虑 :
- 添加稀疏矩阵支持
- 实现增量学习(partial_fit)
- 添加早停机制
-
生产环境准备 :
- 添加模型序列化方法
- 实现scikit-learn兼容API
- 添加详细的文档字符串
10. 从简单实现到工业级应用的演进路径
当这些基础实现需要投入生产环境时,通常需要考虑:
-
性能优化 :
- 使用Cython或Numba加速数值计算
- 实现多线程/多进程并行
- 支持GPU加速(CUDA)
-
功能扩展 :
- 添加正则化项(L1/L2)
- 实现更复杂的分裂准则(Gini, χ²)
- 支持缺失值处理
-
鲁棒性增强 :
- 添加输入数据验证
- 实现更完善的异常处理
- 添加类型提示和单元测试
-
生态集成 :
- 支持scikit-learn的Pipeline
- 添加MLflow等实验跟踪
- 实现ONNX格式导出
实现这些算法只是机器学习工程化的起点。在我的实践中发现,从零实现的过程比调用现成库能学到更多设计决策背后的深层原因。比如为什么逻辑回归默认使用L2正则化?为什么决策树要进行剪枝?这些问题的答案只有在亲手实现时才会真正理解。
更多推荐


所有评论(0)