裸辞后我决定从零学Python机器学习

运营说要今天
2026-07-05 15:05
阅读 870

写在前面:这篇文章写于我裸辞后的第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 训练时间
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提效,那叫给自己找事。

五、接下来打算学什么

休息的这段时间,机器学习算是入了个门。但我知道自己还差得远。接下来打算往这几个方向深入:

  1. 时间序列预测的专门方法:Prophet、ARIMA、Temporal Fusion Transformer等
  2. MLOps:怎么把ML的流程标准化、自动化。这块我觉得和DevOps有很多相通的地方
  3. 大语言模型的应用:现在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

最热最新
暂无评论
运营说要今天Lv.1
0
影响力
0
文章
0
粉丝