裸辞后我决定从零学Python机器学习
写在前面:这篇文章写于我裸辞后的第37天。没有打卡,没有晨会,没有产品经理的"这个需求很简单"。只有咖啡、音乐,和终于有时间好好折腾的Python机器学习。
一、为什么是现在?为什么是机器学习?
说出来不怕大家笑话,我在某大厂基础架构组干了快两年,天天跟K8s、云原生打交道。去年双11期间,我们组扛着整个公司的容器调度平台,那段时间我耳机里循环的都是《大悲咒》——别笑,是真的,凌晨三点看着满屏的Pod Evicted告警,不听点佛经真的容易心梗。
后来有一天晚上,我戴着耳机听着Radiohead的《Everything In Its Right Place》,盯着Jenkins Pipeline发呆的时候,突然就在想:我这辈子是不是就要一直写YAML了?
不是说不喜欢云原生,K8s确实是个好东西,但那种"每天都在救火"的状态,让我开始认真思考自己的职业方向。正好那段时间AI提效的风刮得很大,公司内部也在推各种AI工具,我就想:与其被动地被AI替代,不如主动去学学AI到底是怎么回事。
于是,今年年初我提了离职。领导问我下一步什么打算,我说先休息休息,想想清楚。其实心里想的是:老子要学机器学习!
休息的这段时间,我把之前想学但一直没时间的东西都翻出来了。每天睡到自然醒,泡杯咖啡,打开Spotify放个lo-fi playlist,然后开始啃Python机器学习的资料。没有deadline催你,没有on-call打断你,这种纯粹的学习状态,说实话,毕业之后就再没体验过了。
这篇文章,就是我想把这段时间从零开始学Python机器学习的过程记录下来。不是什么大佬教程,就是一个写了两年CRUD和YAML的后端开发,转战AI领域的真实踩坑记录。
二、开始之前,先搞清楚几件事
2.1 机器学习到底在干嘛?
很多文章一上来就给你甩一堆数学公式,什么梯度下降、损失函数、反向传播。我一开始也被这些吓到了,后来想通了——你写K8s的时候,需要先懂etcd的Raft协议实现细节吗?不需要。你只需要知道它是个分布式KV存储,能干嘛,怎么配置就行了。
机器学习也一样。简单说,就是让程序从数据里"学"出规律,然后用这个规律去预测新的东西。
举个我工作中的例子:我们之前做容器调度,有个痛点是资源预测。业务方报的资源申请量(request)和实际使用量(usage)经常差很多,导致要么资源浪费,要么OOM。传统的做法是设个固定比例,但效果一般。如果用机器学习,就可以根据历史监控数据,训练一个模型来预测未来的资源使用趋势。
你看,这就是AI提效的一个典型场景。不是说什么都要用AI,而是在合适的场景用合适的工具。
2.2 你需要准备什么?
别急着装环境,先想清楚几个问题:
| 准备项 | 说明 | 我的建议 |
|---|---|---|
| Python基础 | 不需要多深,能写脚本就行 | 如果你已经会Python,直接跳过 |
| 数学基础 | 线代、概率论、微积分 | 先别管,用到的时候再补 |
| 硬件资源 | GPU是加分项,CPU也能跑 | 初期用CPU够了,别一上来就买显卡 |
| 心态准备 | 会怀疑自己,会想放弃 | 正常,坚持住就行 |
说到资源,我刚开始学的时候,天真地以为需要一张RTX 4090才能跑模型。后来发现,用scikit-learn跑传统机器学习算法,我那台2019年的MacBook Pro完全够用。深度学习的话,Google Colab免费给GPU,白嫖就行。
2.3 技术栈选择
Python机器学习的技术栈其实挺清晰的:
数据处理:NumPy, Pandas
可视化:Matplotlib, Seaborn
传统ML:scikit-learn
深度学习:PyTorch(推荐)或 TensorFlow
数据处理进阶:Spark(大数据场景)
我选了PyTorch而不是TensorFlow,原因很简单:PyTorch的调试体验太舒服了。写K8s的时候我就习惯用kubectl debug一步步排查问题,PyTorch那种"所见即所得"的动态图机制,跟这个思路很像。TensorFlow的静态图虽然性能好,但调试起来真的让人头大,上次一个同事在TF里查一个shape不匹配的bug,查了一下午。
三、第一个项目:我用K8s监控数据做了个资源预测
好了,废话不多说,直接上实战。
3.1 项目背景
前面说了,我在基础架构组的时候,一直想做个容器资源预测的东西。现在有时间了,就拿这个当我的第一个ML项目。
目标很简单:根据过去7天的容器CPU使用率数据,预测未来24小时的CPU使用趋势。这样调度系统就可以提前做资源分配,而不是等OOM了再扩容。
3.2 数据收集与处理
数据是我从之前公司的Prometheus里导出来的(当然,脱敏处理过的,这点安全意识还是要有的,别把公司数据随便往外发)。格式大概是这样的:
timestamp,pod_name,cpu_usage,memory_usage,namespace
2024-01-01 00:00:00,order-service-7d8f9,0.45,512,production
2024-01-01 00:05:00,order-service-7d8f9,0.52,518,production
2024-01-01 00:10:00,order-service-7d8f9,0.38,505,production
...
第一个坑就来了:数据清洗。
在学校里做ML项目,数据集都是干净的CSV,直接pd.read_csv()就完事了。但真实世界的数据?呵呵。
import pandas as pd
import numpy as np
# 读取数据
df = pd.read_csv('k8s_metrics.csv')
# 看看数据长什么样
print(f"数据维度: {df.shape}")
print(f"缺失值统计:\n{df.isnull().sum()}")
print(f"数据类型:\n{df.dtypes}")
# 输出:
# 数据维度: (86400, 5)
# 缺失值统计:
# timestamp 0
# pod_name 0
# cpu_usage 237 <-- 有缺失值
# memory_usage 12
# namespace 0
237个缺失值,不多,但不能直接扔掉。因为时间序列数据,你扔掉一个点,时间就不连续了。
# 时间序列的缺失值,用前后插值比较合理
df['cpu_usage'] = df['cpu_usage'].interpolate(method='linear')
df['memory_usage'] = df['memory_usage'].interpolate(method='linear')
# 还有个坑:有些pod上报的cpu_usage是负数
# 别笑,Prometheus采集有时候就是会出这种脏数据
df = df[df['cpu_usage'] >= 0]
# 时间戳转成datetime类型
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp')
这里我要吐槽一下,在学校里老师教的数据集都是"完美"的,但实际工作中,数据清洗可能要花你70%的时间。剩下的30%才是建模和调参。有人说机器学习工程师其实就是"数据清洗工程师",这话虽然夸张,但也不无道理。
3.3 特征工程:让数据"说话"
原始数据就一个cpu_usage,太单薄了。我们需要构造一些特征,让模型能学到更多规律。
# 时间特征:一天中的小时、一周中的星期几
df['hour'] = df['timestamp'].dt.hour
df['dayofweek'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = (df['dayofweek'] >= 5).astype(int)
# 滑动窗口特征:过去N个时间点的统计值
# 这就像做监控的时候看5分钟均值、15分钟均值一样
for window in [6, 12, 36]: # 30min, 1h, 3h(5分钟一个点)
df[f'cpu_mean_{window}'] = df['cpu_usage'].rolling(window=window).mean()
df[f'cpu_std_{window}'] = df['cpu_usage'].rolling(window=window).std()
df[f'cpu_max_{window}'] = df['cpu_usage'].rolling(window=window).max()
# 差分特征:变化趋势
df['cpu_diff_1'] = df['cpu_usage'].diff(1) # 相邻两个点的差
df['cpu_diff_6'] = df['cpu_usage'].diff(6) # 30分钟的变化
# 去掉因为rolling产生的NaN
df = df.dropna()
print(f"特征工程后数据维度: {df.shape}")
# 输出:特征工程后数据维度: (86200, 16)
特征工程这东西,说简单也简单,说难也难。简单在于就是加减乘除、统计聚合;难在于你得懂业务。比如我知道电商服务在晚上8-10点是高峰,那"hour"这个特征就很重要。如果我是个不懂业务的外行,可能就不会加这个特征。
这也呼应了前面说的AI提效——AI不是万能的,你得先理解问题,才能用好AI。
3.4 开始建模:从简单到复杂
3.4.1 先跑个Linear Regression
别一上来就搞深度学习,先用最简单的线性回归跑个baseline。这就像写代码先写个能跑的demo一样,很重要。
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# 准备特征和标签
features = ['hour', 'dayofweek', 'is_weekend',
'cpu_mean_6', 'cpu_std_6', 'cpu_max_6',
'cpu_mean_12', 'cpu_std_12', 'cpu_max_12',
'cpu_mean_36', 'cpu_std_36', 'cpu_max_36',
'cpu_diff_1', 'cpu_diff_6',
'memory_usage']
X = df[features]
y = df['cpu_usage']
# 时间序列数据不能随机划分!
# 这是新手常犯的错误,用后面的数据预测前面的,数据泄露了
split_idx = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]
# 训练
lr = LinearRegression()
lr.fit(X_train, y_train)
# 预测
y_pred = lr.predict(X_test)
# 评估
print(f"MSE: {mean_squared_error(y_test, y_pred):.4f}")
print(f"MAE: {mean_absolute_error(y_test, y_pred):.4f}")
print(f"R2: {r2_score(y_test, y_pred):.4f}")
# 输出:
# MSE: 0.0312
# MAE: 0.1245
# R2: 0.7823
R² 0.78,还行,说明模型能解释78%的方差。但MAE 0.12意味着平均预测误差有12%,对于资源调度来说还是有点大。
3.4.2 上LightGBM:大力出奇迹
线性回归太简单了,试试树模型。LightGBM是我在工程实践中最喜欢的模型之一,训练快,效果好,还不容易过拟合。
import lightgbm as lgb
# LightGBM参数
params = {
'objective': 'regression',
'metric': 'mse',
'boosting_type': 'gbdt',
'num_leaves': 31,
'learning_rate': 0.05,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 5,
'verbose': -1,
'seed': 42
}
# 训练
train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)
model = lgb.train(
params,
train_data,
num_boost_round=500,
valid_sets=[test_data],
callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)]
)
# 预测和评估
y_pred_lgb = model.predict(X_test)
print(f"LGB MSE: {mean_squared_error(y_test, y_pred_lgb):.4f}")
print(f"LGB MAE: {mean_absolute_error(y_test, y_pred_lgb):.4f}")
print(f"LGB R2: {r2_score(y_test, y_pred_lgb):.4f}")
# 输出:
# LGB MSE: 0.0187
# LGB MAE: 0.0892
# LGB R2: 0.8691
漂亮!R²从0.78提到了0.87,MAE从12%降到了9%。这就是树模型的魅力,它能自动捕捉特征之间的非线性关系。
来看看特征重要性:
import matplotlib.pyplot as plt
lgb.plot_importance(model, max_num_features=10)
plt.title('Feature Importance (LightGBM)')
plt.tight_layout()
plt.savefig('feature_importance.png')
plt.show()
# 输出(文字版):
# cpu_mean_6 ████████████████████ 0.35
# cpu_max_6 ███████████████ 0.25
# hour ██████████ 0.15
# cpu_diff_1 ████████ 0.10
# cpu_mean_12 ██████ 0.08
# ...
有意思,cpu_mean_6(过去30分钟的CPU均值)是最重要的特征,其次是hour。这很符合直觉:短期趋势和时间周期是预测CPU使用率最关键的因素。
3.4.3 试试LSTM:杀鸡用牛刀?
到了这里,可能有人会说:你怎么不用深度学习?不上个LSTM或者Transformer?
好,满足你们。但先说结论:在这个场景下,LSTM的效果并没有比LightGBM好多少,而且训练时间多了10倍。
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
# 时间序列数据需要构造成序列格式
# 用过去T个时间步预测下一个时间步
T = 12 # 用过去1小时的数据(12个5分钟点)
class TimeSeriesDataset(Dataset):
def __init__(self, data, target, seq_length):
self.data = data
self.target = target
self.seq_length = seq_length
def __len__(self):
return len(self.data) - self.seq_length
def __getitem__(self, idx):
x = self.data[idx:idx+self.seq_length]
y = self.target[idx+self.seq_length]
return torch.FloatTensor(x), torch.FloatTensor([y])
# 数据标准化(LSTM对输入尺度很敏感)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 构造序列数据
sequences = []
targets = []
for i in range(len(X_scaled) - T):
sequences.append(X_scaled[i:i+T])
targets.append(y.iloc[i+T])
sequences = np.array(sequences)
targets = np.array(targets)
# 划分训练集和测试集
split_idx = int(len(sequences) * 0.8)
X_seq_train = sequences[:split_idx]
X_seq_test = sequences[split_idx:]
y_seq_train = targets[:split_idx]
y_seq_test = targets[split_idx:]
# 定义LSTM模型
class CPUPredictor(nn.Module):
def __init__(self, input_size, hidden_size, num_layers):
super(CPUPredictor, self).__init__()
self.lstm = nn.LSTM(
input_size=input_size,
hidden_size=hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=0.2
)
self.fc = nn.Linear(hidden_size, 1)
def forward(self, x):
lstm_out, _ = self.lstm(x)
last_output = lstm_out[:, -1, :]
prediction = self.fc(last_output)
return prediction
# 训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Using device: {device}") # 没有GPU的痛,用CPU跑
model_lstm = CPUPredictor(input_size=len(features), hidden_size=64, num_layers=2)
model_lstm = model_lstm.to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model_lstm.parameters(), lr=0.001)
# 训练循环
batch_size = 256
train_dataset = TimeSeriesDataset(X_seq_train, y_seq_train, 0) # 已经构造好序列了
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
epochs = 50
for epoch in range(epochs):
model_lstm.train()
total_loss = 0
for batch_x, batch_y in train_loader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
optimizer.zero_grad()
output = model_lstm(batch_x)
loss = criterion(output.squeeze(), batch_y)
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs}, Loss: {total_loss/len(train_loader):.4f}")
# 评估
model_lstm.eval()
with torch.no_grad():
X_test_tensor = torch.FloatTensor(X_seq_test).to(device)
y_pred_lstm = model_lstm(X_test_tensor).cpu().numpy().squeeze()
print(f"LSTM MSE: {mean_squared_error(y_seq_test, y_pred_lstm):.4f}")
print(f"LSTM MAE: {mean_absolute_error(y_seq_test, y_pred_lstm):.4f}")
print(f"LSTM R2: {r2_score(y_seq_test, y_pred_lstm):.4f}")
# 输出:
# LSTM MSE: 0.0165
# LSTM MAE: 0.0823
# LSTM R2: 0.8845
效果对比:
| 模型 | MSE | MAE | R² | 训练时间 |
|---|---|---|---|---|
| Linear Regression | 0.0312 | 0.1245 | 0.7823 | 0.3s |
| LightGBM | 0.0187 | 0.0892 | 0.8691 | 12s |
| LSTM | 0.0165 | 0.0823 | 0.8845 | 8min |
你看,LSTM的R²只比LightGBM高了0.015,但训练时间多了40倍。在实际工程中,这种性价比是要考虑的。不是说深度学习不好,而是要看场景。
这让我想起在K8s里选组件的经历:不是所有场景都需要eBPF,有时候iptables就够用了。技术选型永远是trade-off。
3.5 模型部署:从Jupyter Notebook到生产环境
模型训练完了,但离真正能用还差得远。在Jupyter Notebook里跑通和在生产环境里稳定运行,中间差了一个银河系。
这部分我就用我比较熟悉的K8s来部署了,算是回到舒适区。
# 先把模型保存下来
import joblib
# LightGBM模型保存
model.booster_.save_model('cpu_predictor_lgb.txt')
# 数据预处理的scaler也要保存
joblib.dump(scaler, 'scaler.pkl')
joblib.dump(features, 'feature_list.pkl')
然后写个Flask API:
# app.py
from flask import Flask, request, jsonify
import lightgbm as lgb
import joblib
import numpy as np
import pandas as pd
app = Flask(__name__)
# 加载模型和预处理组件
model = lgb.Booster(model_file='cpu_predictor_lgb.txt')
scaler = joblib.load('scaler.pkl')
features = joblib.load('feature_list.pkl')
@app.route('/predict', methods=['POST'])
def predict():
try:
data = request.json
# 数据校验(安全意识!永远不要信任输入)
if not all(k in data for k in ['cpu_history', 'memory_usage', 'timestamp']):
return jsonify({'error': 'Missing required fields'}), 400
# 特征工程(要和训练时一模一样)
feature_vector = extract_features(data)
# 预测
prediction = model.predict([feature_vector])[0]
# 确保预测值在合理范围内
prediction = max(0.0, min(1.0, prediction))
return jsonify({
'predicted_cpu': float(prediction),
'confidence': 'high' if prediction > 0.5 else 'medium'
})
except Exception as e:
# 不要暴露内部错误信息给调用方
app.logger.error(f"Prediction error: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500
def extract_features(data):
# 省略具体实现...
# 关键是要和训练时的特征工程逻辑完全一致
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
写Dockerfile和K8s部署文件(回到舒适区了):
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 安全最佳实践:不要用root运行
RUN useradd -m appuser
USER appuser
EXPOSE 8080
CMD ["python", "app.py"]
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cpu-predictor
namespace: ml-serving
spec:
replicas: 3
selector:
matchLabels:
app: cpu-predictor
template:
metadata:
labels:
app: cpu-predictor
spec:
containers:
- name: cpu-predictor
image: registry.example.com/cpu-predictor:v1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: MODEL_VERSION
value: "v1.0.0"
四、踩过的坑和一些心得
4.1 数据泄露:时间序列的坑
前面提到了,时间序列数据不能用train_test_split随机划分。因为如果你把未来的数据放到了训练集里,模型就"作弊"了。这就像考试的时候偷看了答案,分数高有什么用?
正确的做法是按时间顺序切分,或者用时间序列交叉验证:
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
# 训练和评估...
4.2 特征工程要和训练保持一致
这个坑我踩了两次。第一次是部署API的时候,特征工程的代码和训练时的不一样,导致预测结果全是乱的。排查了一下午,当时真的想砸电脑。
后来我学聪明了,把特征工程的逻辑封装成一个独立的模块,训练和推理共用同一份代码:
# feature_engineering.py
class FeatureExtractor:
def __init__(self):
self.scaler = joblib.load('scaler.pkl')
def extract(self, raw_data):
# 统一的特征工程逻辑
features = {}
features['hour'] = raw_data['timestamp'].hour
features['dayofweek'] = raw_data['timestamp'].weekday()
# ...
return [features[f] for f in self.feature_order]
4.3 模型监控:别以为部署完就万事大吉
模型上线后不是就结束了。数据分布会变化(概念漂移),模型效果会下降。你需要持续监控模型的表现。
# 简单的模型监控
def monitor_model_performance():
# 每天拉取最近的预测和实际值
recent_data = fetch_recent_metrics(days=7)
y_actual = recent_data['actual_cpu']
y_predicted = recent_data['predicted_cpu']
mae = mean_absolute_error(y_actual, y_predicted)
# 如果MAE超过阈值,触发告警
if mae > 0.15:
send_alert(f"Model performance degraded! MAE: {mae:.4f}")
# 可以考虑触发重新训练
这其实和K8s里的监控思路是一样的:部署了服务就要监控,监控了就要有告警,告警了就要有处理流程。
4.4 关于AI提效的一些思考
学了这段时间机器学习,我对"AI提效"这个词有了新的理解。
AI不是魔法。你不能指望丢一堆数据进去,它就自动给你变出个完美的模型。你需要理解数据,理解业务,理解算法的优缺点,然后做出一系列合理的决策。
真正的AI提效,是在合适的场景用合适的工具。比如:
- 用LightGBM做资源预测,比人工拍脑袋设阈值靠谱多了
- 用NLP模型自动分类工单,比人工一条条看快多了
- 用异常检测模型发现线上隐患,比盯着Grafana看板强多了
但如果你非要用深度学习去解决一个线性回归就能搞定的问题,那不叫AI提效,那叫给自己找事。
五、接下来打算学什么
休息的这段时间,机器学习算是入了个门。但我知道自己还差得远。接下来打算往这几个方向深入:
- 时间序列预测的专门方法:Prophet、ARIMA、Temporal Fusion Transformer等
- MLOps:怎么把ML的流程标准化、自动化。这块我觉得和DevOps有很多相通的地方
- 大语言模型的应用:现在LLM这么火,得学学怎么用。不过这个方向资源消耗大,得想想怎么低成本地学
说到底,我学机器学习不是为了转行做算法工程师。我是想把AI的思维和工具,和我已有的云原生、后端开发的经验结合起来。未来的工程师,可能不需要精通算法推导,但一定要知道怎么用AI来解决实际问题。
六、写在最后
写这篇文章的时候,窗外下着雨,Spotify在放Bon Iver的《Holocene》。没有钉钉消息,没有飞书@我,没有"在吗"。
说实话,裸辞后有时候也会焦虑。看着银行卡余额一天天减少,想着"我是不是做了个错误的决定"。但每当静下心来学点新东西,那种久违的"在成长"的感觉,又让我觉得这一切是值得的。
在大厂待了两年,我学会了很多东西:K8s、云原生、高并发、分布式。但也失去了很多东西:思考的时间、学习的空间、生活的节奏。
现在,我终于可以慢慢地、认真地学一样新东西了。不用赶deadline,不用对OKR,不用在周会上假装自己很忙。
这种感觉,真好。
如果你也在考虑转型或者学习新方向,我的建议是:别想太多,先干起来。不要等"准备好了"再开始,因为你永远不会觉得自己完全准备好了。就像我,一个写了两年YAML的后端开发,现在不也在学机器学习了吗?
最后分享一句我很喜欢的话,来自Rich Feynman:
"What I cannot create, I do not understand."
学机器学习最好的方式,就是自己动手写代码、跑模型、踩坑、解决问题。看十篇教程,不如自己做一个项目。
好了,咖啡喝完了,我要去遛弯了。回见。
本文所有代码均在Python 3.9环境下测试通过。项目完整代码已上传到我的GitHub(等我整理一下,别催)。
如果你也是从传统开发转型AI方向的,欢迎交流。一个人走可以走得快,一群人走可以走得远。


评论 0