在职前端Leader学习/转行 AI Agent -DAY61

0 阅读8分钟

2026.09.22 09:49

学习路线

Python -> Python数据分析 -> LangChain -> LangGraph -> FastAPI + SQLAIchemy -> 机器学习 -> 深度学习 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding -> Hermes -> Docker

复习巩固总结

1. 编码六步走

  1. 加载数据
  2. 数据的预处理,切分测试集和训练集
  3. 特征工程:特征提取,特征预处理(标准化/归一化)
  4. 模型训练(保存模型)
  5. 模型预测(使用模型)
  6. 模型评估

2. 有监督学习

2.1 回归

2.1.1 KNN

不常用

from sklearn.neighbors import KNeighborsRegressor  # KNN算法的 回归模型
x_train = [[0, 0, 1], [1, 1, 0], [3, 10, 10], [4, 11, 12]]  # 训练集的特征数据,因为特征可以有多个特征,所以是一个二维数组
y_train = [0.1, 0.2, 0.3, 0.4]                              # 训练集的标签数据,因为标签是连续的,所以是一个一维数组
x_test = [[3, 11, 10]]
estimator = KNeighborsRegressor(n_neighbors=3)
estimator.fit(x_train, y_train)
y_pre = estimator.predict(x_test)
print(f"预测值为:{y_pre}")
2.1.2 一元线性回归
2.1.2.1 正规方程法

波士顿房价预测

from sklearn.preprocessing import StandardScaler        # 特征处理
from sklearn.model_selection import train_test_split    # 数据集划分
from sklearn.linear_model import LinearRegression       # 正规方程的回归模型
from sklearn.linear_model import SGDRegressor           # 梯度下降的回归模型
from sklearn.metrics import mean_absolute_error, mean_squared_error, root_mean_squared_error          # 均方误差评估
from sklearn.linear_model import Ridge, RidgeCV         # 岭回归
import pandas as pd                                     # 数据处理
import numpy as np                                      # 数组处理
data_url = "https://raw.githubusercontent.com/Csengupta1101/Datasets/main/BostonHousing.csv"
raw_df = pd.read_csv(data_url, storage_options={"User-Agent": "Mozilla/5.0"})
data = raw_df.drop(columns=["medv"]).values   # 前 13 列是特征
target = raw_df["medv"].values                # 最后一列房价是标签
x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=23)
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_test = transfer.transform(x_test)
estimator = LinearRegression(fit_intercept=True)  # fit_intercept=True:是否需要截距(Bias,偏置),默认是True
estimator.fit(x_train, y_train)
print(f'权重:{estimator.coef_}')
print(f'偏置:{estimator.intercept_}')
y_pre = estimator.predict(x_test)
print(f'均方误差:{mean_squared_error(y_test, y_pre)}')         # MSE:均方误差,公式:每个样本的误差平方和 / 样本总数
print(f'均方根误差:{root_mean_squared_error(y_test, y_pre)}')  # RMSE:均方根误差,公式:每个样本的误差平方和 / 样本总数,开平方根
print(f'平均绝对误差:{mean_absolute_error(y_test, y_pre)}')    # MAE:平均绝对误差,公式:每个样本的误差绝对值和 / 样本总数
2.1.2.2 梯度下降法

波士顿房价预测

from sklearn.preprocessing import StandardScaler        # 特征处理
from sklearn.model_selection import train_test_split    # 数据集划分
from sklearn.linear_model import LinearRegression       # 正规方程的回归模型
from sklearn.linear_model import SGDRegressor           # 梯度下降的回归模型
from sklearn.metrics import mean_absolute_error, mean_squared_error, root_mean_squared_error          # 均方误差评估
from sklearn.linear_model import Ridge, RidgeCV         # 岭回归
import pandas as pd                                     # 数据处理
import numpy as np                                      # 数组处理
data_url = "https://raw.githubusercontent.com/Csengupta1101/Datasets/main/BostonHousing.csv"
raw_df = pd.read_csv(data_url, storage_options={"User-Agent": "Mozilla/5.0"})
data = raw_df.drop(columns=["medv"]).values   # 前 13 列是特征
target = raw_df["medv"].values                # 最后一列房价是标签
x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=23)
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_test = transfer.transform(x_test)
estimator = SGDRegressor(fit_intercept=True, learning_rate="constant", eta0=0.01)
estimator.fit(x_train, y_train)
print(f'权重:{estimator.coef_}')
print(f'偏置:{estimator.intercept_}')
y_pre = estimator.predict(x_test)
print(f'均方误差:{mean_squared_error(y_test, y_pre)}')            # 参1:测试集的真实标签,参2:测试集的预测标签
print(f'均方根误差:{root_mean_squared_error(y_test, y_pre)}')     # 参1:测试集的真实标签,参2:测试集的预测标签
print(f'平均绝对误差:{mean_absolute_error(y_test, y_pre)}')       # 参1:测试集的真实标签,参2:测试集的预测标签
2.1.3 多元线性回归

波士顿房价预测,使用的是梯度下降法

from sklearn.preprocessing import StandardScaler        # 特征处理
from sklearn.model_selection import train_test_split    # 数据集划分
from sklearn.linear_model import LinearRegression       # 正规方程的回归模型
from sklearn.linear_model import SGDRegressor           # 梯度下降的回归模型
from sklearn.metrics import mean_absolute_error, mean_squared_error, root_mean_squared_error          # 均方误差评估
from sklearn.linear_model import Ridge, RidgeCV         # 岭回归
import pandas as pd                                     # 数据处理
import numpy as np                                      # 数组处理
data_url = "https://raw.githubusercontent.com/Csengupta1101/Datasets/main/BostonHousing.csv"
raw_df = pd.read_csv(data_url, storage_options={"User-Agent": "Mozilla/5.0"})
data = raw_df.drop(columns=["medv"]).values   # 前 13 列是特征
target = raw_df["medv"].values                # 最后一列房价是标签
x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=23)
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_test = transfer.transform(x_test)
estimator = SGDRegressor(fit_intercept=True, learning_rate="constant", eta0=0.01)
estimator.fit(x_train, y_train)
print(f'权重:{estimator.coef_}')
print(f'偏置:{estimator.intercept_}')
y_pre = estimator.predict(x_test)
print(f'均方误差:{mean_squared_error(y_test, y_pre)}')            # 参1:测试集的真实标签,参2:测试集的预测标签
print(f'均方根误差:{root_mean_squared_error(y_test, y_pre)}')     # 参1:测试集的真实标签,参2:测试集的预测标签
print(f'平均绝对误差:{mean_absolute_error(y_test, y_pre)}')       # 参1:测试集的真实标签,参2:测试集的预测标签

2.2 分类

2.2.1 KNN

不常用

from sklearn.neighbors import KNeighborsClassifier
x_train = [[0], [1], [2], [3]]  # 训练集特征数据,因为特征可以有多个特征,所以是一个二维数组
y_train = [0, 0, 1, 1]          # 训练集标签数据,因为标签是离散的,所以是一个一维数组
x_test = [[5]]                  # 测试集特征数据
estimator = KNeighborsClassifier(n_neighbors=2)
estimator.fit(x_train, y_train)
y_pre = estimator.predict(x_test)
print(f"预测值为:{y_pre}")
2.2.2 逻辑回归

逻辑回归 = 有监督学习,有特征、有标签,且标签是离散的(分类)= 适用于二分类。

电信流失用户预测

def dm03_logistic_regression():
    churn_df = pd.read_csv(Path(__file__).resolve().parent / 'data' / 'churn.csv')
    churn_df = pd.get_dummies(churn_df, columns=['Churn', 'gender'])
    churn_df.drop(['Churn_No', 'gender_Male'], axis=1, inplace=True)
    churn_df.rename(columns={'Churn_Yes': 'flag'}, inplace=True)
    x = churn_df[['Contract_Month', 'internet_other', 'PaymentElectronic']]
    y = churn_df['flag']  # False -> 不流失,True -> 流失
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23)
    estimator = LogisticRegression()
    estimator.fit(x_train, y_train)
    y_pre = estimator.predict(x_test)
    print(f"预测值:{y_pre}")
    print(f"准确率:{estimator.score(x_test, y_test)}")  # 预测前 0.7679205110007097
    print(f"准确率:{accuracy_score(y_test, y_pre)}")    # 预测后 0.7679205110007097
    print(f"精确率:{precision_score(y_test, y_pre)}")   # 0.580769230769230
    print(f"召回率:{recall_score(y_test, y_pre)}")      # 0.4092140921409214
    print(f"F1值:{f1_score(y_test, y_pre)}")           # 0.48012718600953896
    print(f"分类评估报告:\n{classification_report(y_test, y_pre)}")
2.2.3 决策树
2.2.3.1 C4.5树

不常用,没有实现案例,但是可以将 CART 树的示例 改成使用 C4.5 树:

estimator = DecisionTreeClassifier(criterion="entropy", max_depth=10)

但这样改也只是分裂指标换成熵,树还是二叉 CART:没有信息增益率,也不会按 C4.5 那样一次多叉、用特征熵去压「取值多的特征」

2.2.3.2 CART树

泰坦尼克号案例

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
from pathlib import Path
path = Path(__file__).resolve().parent / 'data' / 'train.csv'
data = pd.read_csv(path)
x = data[['Pclass', 'Sex', 'Age']]
y = data['Survived']
x = x.copy()                                      # 拷贝数据,不写也行
x['Age'] = x['Age'].fillna(x['Age'].mean())
x = pd.get_dummies(x, columns=['Sex'])
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23)
estimator = DecisionTreeClassifier(max_depth=10)
estimator.fit(x_train, y_train)
y_pre = estimator.predict(x_test)
print(f'预测值:{y_pre}')
print(f'分类评估报告:\n{classification_report(y_test, y_pre)}')
plt.figure(figsize=(30, 20))  # 设置图片大小,30 * 100(dpi) * 20 * 100(dpi) = 3000 * 2000像素
plot_tree(estimator, filled=True, max_depth=10)
plt.savefig(Path(__file__).resolve().parent / 'data' / 'my_titanic.png')
plt.show()
2.2.4 朴素贝叶斯

商品评论感情分析

import numpy as np                              # 数学计算包
import pandas as pd                             # 数据处理包
import matplotlib.pyplot as plt                 # 画图包
import jieba                                    # 分词包
from sklearn.feature_extraction.text import CountVectorizer # 词频统计包,把评论内容 转成 词频矩阵.
from sklearn.metrics import accuracy_score
from sklearn.naive_bayes import MultinomialNB               # 朴素贝叶斯对象
from pathlib import Path
df = pd.read_csv(Path(__file__).parent / "data" / "书籍评价.csv", encoding="gbk")
df["labels"] = np.where(df["评价"] == "好评", 1, 0)
y = df["labels"]
comment_list = [','.join(jieba.lcut(line)) for line in df['内容'].values]
print(comment_list)
with open(Path(__file__).parent / "data" / "stopwords.txt", "r", encoding="utf-8") as src_f:
    stopwords_list = src_f.readlines()
    stopwords_list = [line.strip() for line in stopwords_list]
    stopwords_list = list(set(stopwords_list))
    print(stopwords_list)
transfer = CountVectorizer(stop_words=stopwords_list)  # 参数:停用词列表
x = transfer.fit_transform(comment_list).toarray()
x_train = x[:10]
y_train = y[:10]
x_test = x[10:]
y_test = y[10:]
estimator = MultinomialNB()  # 创建朴素贝叶斯模型对象
estimator.fit(x_train, y_train)  # 模型训练
y_pre = estimator.predict(x_test)
print(f'模型预测结果:{y_pre}')
print(f'模型准确率为:{accuracy_score(y_test, y_pre)}')

3. 无监督学习 - 聚类

根据样本之间的相似性,把样本划到不同类别里。相似度怎么算,团就怎么变。常用的是欧氏距离(不是唯一的,曼哈顿、余弦等也能当尺子)。

目的:在没有先验知识(没有现成标签)的情况下,自动发现数据里的内在结构和模式。所以是无监督学习

实现方案:KMeans算法

3.1 KMeans

3.1.1 底层实现

举例:15 个点,K=2K=2

xxyyxxyy
P177P8910
P223P9107
P368P1055
P414P1176
P512P1293
P631P1328
P788P14511
P1552

第一步:事先确定常数 KK:最终要几簇,几个质心。API 里就是 n_clusters

对于此例,K=2K=2 K=2K=2

第二步:随机选 KK 个样本点当初始聚类中心

对于此例,初始中心取 P1(7,7)(7,7)、P2(2,3)(2,3)

第三步:每个样本算到 KK 个中心的距离,跟最近的那个中心,算这个簇

例如 P3 到 P1:

(67)2+(87)2=21.41\sqrt{(6-7)^2+(8-7)^2}=\sqrt{2}\approx 1.41
P1(7,7)(7,7)P2(2,3)(2,3)跟谁P1P2跟谁
P31.416.40P1P938.94P1
P46.711.41P2P102.833.61P1
P57.811.41P2P1115.83P1
P67.212.24P2P124.477.00P1
P71.417.81P1P135.105.00P2
P83.619.90P1P144.478.54P1
P155.393.16P2
  • P1 簇:P1、P3、P7、P8、P9、P10、P11、P12、P14
  • P2 簇:P2、P4、P5、P6、P13、P15

第四步:每个簇用样本坐标的均值当新中心:新中心和旧中心一样 → 停;否则回到第 3 步,直到中心不再变

P1 簇 9 个点:x=66\sum x=66y=65\sum y=65

P1=(669,659)(7.3, 7.2)P'_1=\Bigl(\frac{66}{9},\frac{65}{9}\Bigr)\approx(7.3,\ 7.2)

P2 簇 6 个点:x=14\sum x=14y=20\sum y=20,均值 (2.3, 3.3)\approx(2.3,\ 3.3)。课件写成 (1.8, 4.6)(1.8,\ 4.6),后面距离表按课件这两个数走,谁跟谁簇不变

旧中心 (7,7)(7,7)(2,3)(2,3) 和新的不一样,还没停。所以用新中心再算一遍距离、再贴标签。

P1(7.3,7.2)P'_1(7.3,7.2)P2(1.8,4.6)P'_2(1.8,4.6)跟谁P1P'_1P2P'_2跟谁
P10.365.73P1P'_1P83.249.00P1P'_1
P26.751.61P2P'_2P92.828.54P1P'_1
P31.395.40P1P'_1P103.183.22P1P'_1
P47.021.00P2P'_2P111.325.39P1P'_1
P58.162.72P2P'_2P124.667.38P1P'_1
P67.573.79P2P'_2P135.253.41P2P'_2
P71.067.07P1P'_1P144.307.16P1P'_1
P155.253.41P2P'_2

簇成员和上一轮一样。中心再按均值更新;若再算还是这两拨人,中心不再挪,收敛。平面上左边一坨(P2 那簇)、右边一坨(P1 那簇)。

3.1.2 代码实现
"""
案例:
    演示Kmeans聚类算法入门案例

Kmeans简介:
    它属于无监督学习,即:有特征,无标签,根据样本间的相似性进行划分
    所谓的相似性可以理解为就是距离,例如:欧氏距离,曼哈顿(城市街区)举例,切比雪夫距离,闵式距离...

    一般大厂,项目初期在没有先备知识(标签)的情况下,可能会用
"""
import os
os.environ['OMP_NUM_THREADS'] = '4'                     # OpenMP多任务程序,这里设置为4个线程,防止出现线程冲突等
# 导包
from sklearn.cluster import KMeans                      # 聚类的API,采用指定质心来分簇
import matplotlib.pyplot as plt                         # 绘图的
from sklearn.datasets import make_blobs                 # 默认会按照高斯分布(正态分布)生成数据集,只需要指定均值,标准差
from sklearn.metrics import calinski_harabasz_score     # 评价指标,值越大,聚类效果越好

# 1. 准备数据集
# 参1:样本数量  参2:样本特征数量(2列)  参3:样本标签数量  参4:标准差  参5:随机种子
x, y = make_blobs(n_samples=1000, n_features=2, centers=[[-1, -1], [0, 0], [1, 1], [2, 2]], cluster_std=[0.4, 0.2, 0.3, 0.4], random_state=23)
print(x)
print(y)

# 2. 绘制上述的图形
# 参1:横坐标  参2:纵坐标  参3:颜色
plt.scatter(x[:, 0], x[:, 1])
plt.show()

# 3. 创建Kmeans对象
# 参1:聚类数量  参2:随机种子
estimator = KMeans(n_clusters=4, random_state=23)

# 4. 模型训练和预测
y_pre = estimator.fit_predict(x)  # 预测值

# 5. 绘图预测结果
plt.scatter(x[:, 0], x[:, 1], c=y_pre)
plt.show()

# 6. 评价指标
print(f'评价指标:{calinski_harabasz_score(x, y_pre)}')  # 越大越好
干什么
n_clusters开始时的聚类中心个数,也就是准备划几簇、几个质心(centroids)。课件默认 8
fit(x)只根据 xx 算质心、把训练样本归簇。没有 y
predict(x)已经有质心之后,看每个样本离哪个质心近,吐簇编号
fit_predict(x)fitpredict,算中心并给出每个样本属于哪一类

3.2 评估

3.2.1 SSE + 肘方法
3.2.1.1 SSE
SSE=i=1kpCipmi2SSE=\sum_{i=1}^{k}\sum_{p\in C_i}\lvert p-m_i\rvert^{2}
符号是什么
CiC_iii 个簇
kk聚类中心个数,就是 n_clusters
pp某个簇里的一个样本
mim_iii 簇的质心。课件写 mm,下标跟簇走

三层套娃,从里往外:

  1. 一个样本的误差平方pmi2\lvert p-m_i\rvert^{2}。课件口诀:真实值 − 质心 = 误差,再平方。图上就是点到红质心那根线的长度平方
  2. 一个簇:把该簇所有 pp 的误差平方加起来(公式里层 pCi\sum_{p\in C_i}
  3. 整份数据:再对 kk 个簇求和(外层 i=1k\sum_{i=1}^{k})= SSE

SSE 越小,点越靠近自己的中心,内聚越高,聚类效果越好。 不是「越小 kk 越好」——下面肘方法会看到,kk 加大 SSE 几乎总会掉。

3.2.1.2 肘方法:用 SSE 定 kk

nn 个点的数据集,kk 从 1 扫到 nn,每次聚完算一次 SSE:

  • k=1k=1:全员一个中心,点离中心远,SSE 最大
  • kk 往上加:多几个中心,点能跟更近的那个,SSE 单调往下掉
  • k=nk=n:每个点自己当中心,pmi=0\lvert p-m_i\rvert=0,SSE = 0。

横轴 kk、纵轴代价(SSE / Cost JJ),曲线先陡后平。下降率突然变缓的拐点当成最佳 n_clusters——像胳膊肘,所以叫肘。

3.2.1.3 代码实现
"""
案例:
    演示聚类算法的评估指标,即:SSE + 肘部法,SC轮廓系数法,CH轮廓系数法

聚类算法的评估指标:
    思路1:SSE + 肘部法
        SSE:
            概述:
                所有簇的所有样本到该质心的误差的平方和
            特点:
                随着K值的增加,SSE值会逐渐减少
            目标:
                SSE值越小,代表簇内样本越聚集,内聚程度越高
        肘部法:
            K值增大,SSE值会随之减小,下降梯度陡然变缓的时候,那个K值,就是我们要的最佳值
"""
import os
os.environ['OMP_NUM_THREADS'] = '4'  # 设置OMP程序运行时使用的线程数

# 导包
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.metrics import calinski_harabasz_score

# 1. 定义函数,演示:SSE + 肘部法
def dm01_sse():
    # 1. 定义sse列表,记录:每个K值的 SSE值
    sse_list = []

    # 2. 生成数据集
    # 参1:样本数量  参2:特征数量  参3:4个簇  参4:4个簇的std标准差  参5:固定随机种子
    x, y = make_blobs(
        n_samples=1000, 
        n_features=2, 
        centers=[[-1, -1], [0, 0], [1, 1], [2, 2]], 
        cluster_std=[0.4, 0.2, 0.2, 0.2], 
        random_state=23
    )

    # 3. for循环遍历,获取到每个K值,计算其对应的SSE值,并添加到sse_list列表中
    for k in range(1, 100):
        # 3.1 创建KMeans对象,参1:聚的数量,参2:最大迭代次数,参3:固定随机种子
        estimator = KMeans(n_clusters=k, max_iter=100, random_state=23)
        # 3.2 模型训练
        estimator.fit(x)
        # 3.3 模型预测
        # 3.4 获取到每个簇的SSE值
        sse_value = estimator.inertia_
        # 3.5 将每个K值对应的sse值,添加到sse_list列表中
        sse_list.append(sse_value)

    # 4. 绘制sse曲线 -> 数据的可视化
    # print(f'sse_list列表:{sse_list}')
    # 4.1 创建画布,指定:画布的尺寸
    plt.figure(figsize=(20, 10))
    # 4.2 设置标题
    plt.title('SSE value')
    # 4.3 设置x轴的刻度
    plt.xticks(range(1, 100, 3))
    # 4.4 添加x轴,y轴的标签
    plt.xlabel('K')
    plt.ylabel('SSE')
    # 4.5 绘制网格
    plt.grid()
    # 4.6 绘制折线图
    # 参1:K值  参2:该K值对应的SSE值
    plt.plot(range(1, 100), sse_list)
    plt.show()

# 2. 定义函数,演示:SC轮廓系数法

# 3. 定义函数,演示:CH轮廓系数法

# 4. 测试
if __name__ == '__main__':
    dm01_sse()
3.2 SC
3.2.1 底层实现

每一个样本 ii 算一个 SS,再对所有样本取平均。

S=bamax(a,b),S[1, 1]S=\frac{b-a}{\max(a,b)},\qquad S\in[-1,\ 1]
名字算什么好坏
aa内聚 Cohesionii同簇其他点的平均距离越小越好:簇内越像
bb分离 Separationii最近另一簇里所有点的平均距离(各簇先各自平均,再取最小那个)越大越好:越不像别的簇、耦合越低

分母取 max(a,b)\max(a,b) 是为了把结果压进 [1,1][-1,1]

  • b>ab>a(离别的簇更远):S=(ba)/b=1a/bS=(b-a)/b=1-a/b,贴 1
  • a>ba>b(其实更该跟邻居簇):S=(ba)/a=b/a1S=(b-a)/a=b/a-1,往 −1 掉
  • a=ba=bS=0S=0,卡在边界上
SS 靠近含义
11aba\ll b,自己这簇挤、离别人远,分对了
00aba\approx b,两簇边上,可去可留
1-1aba\gg b,跟错簇了

整份数据的 SC = 所有样本 SS 的平均。越大越好。 和 SSE「越小越好」方向相反;和肘方法搭配时,常扫一圈 kk,看哪一个 kk 的平均轮廓系数最高。

3.2.2 代码实现
# 2. 定义函数,演示:SC轮廓系数法
def dm02_sc():
    # 1. 定义sse列表,记录:每个K值的 SSE值
    sc_list = []

    # 2. 生成数据集
    # 参1:样本数量  参2:特征数量  参3:4个簇  参4:4个簇的std标准差  参5:固定随机种子
    x, y = make_blobs(
        n_samples=1000, 
        n_features=2, 
        centers=[[-1, -1], [0, 0], [1, 1], [2, 2]], 
        cluster_std=[0.4, 0.2, 0.2, 0.2], 
        random_state=23
    )

    # 3. for循环遍历,获取到每个K值,计算其对应的SSE值,并添加到sse_list列表中
    # for k in range(1, 100):
    for k in range(2, 100):  # 考虑簇外,至少2个簇
        # 3.1 创建KMeans对象,参1:聚的数量,参2:最大迭代次数,参3:固定随机种子
        estimator = KMeans(n_clusters=k, max_iter=100, random_state=23)
        # 3.2 模型训练
        estimator.fit(x)
        # 3.3 模型预测
        y_pre = estimator.predict(x)
        # 3.4 获取到每个簇的SSE值
        # sc_value = estimator.inertia_
        sc_value = silhouette_score(x, y_pre)
        # 3.5 将每个K值对应的sse值,添加到sse_list列表中
        sc_list.append(sc_value)

    # 4. 绘制sse曲线 -> 数据的可视化
    # print(f'sc_list列表:{sc_list}')
    # 4.1 创建画布,指定:画布的尺寸
    plt.figure(figsize=(20, 10))
    # 4.2 设置标题
    plt.title('SC value')
    # 4.3 设置x轴的刻度
    plt.xticks(range(1, 100, 3))
    # 4.4 添加x轴,y轴的标签
    plt.xlabel('K')
    plt.ylabel('SC')
    # 4.5 绘制网格
    plt.grid()
    # 4.6 绘制折线图
    # 参1:K值  参2:该K值对应的SSE值
    # plt.plot(range(1, 100), sc_list)
    plt.plot(range(2, 100), sc_list)
    plt.show()
3.3 CH
3.3.1 底层实现

口诀:类内距离平方和越小越好,类间距离平方和越大越好,种类数少点更好。

CH(k)=SSBSSWmkk1\mathrm{CH}(k)=\frac{\mathrm{SSB}}{\mathrm{SSW}}\cdot\frac{m-k}{k-1}
SSW=i=1mxiCpi2,SSB=j=1knjCjXˉ2\mathrm{SSW}=\sum_{i=1}^{m}\lVert x_i-C_{p_i}\rVert^{2},\qquad \mathrm{SSB}=\sum_{j=1}^{k}n_j\lVert C_j-\bar{X}\rVert^{2}
符号是什么
xix_iii 个样本
CpiC_{p_i}xix_i 所在簇的质心
CjC_jjj 簇的质心
Xˉ\bar{X}全体样本均值(按簇大小加权后,等于各质心的加权平均)
njn_jjj 簇的样本数
mm总样本数
kk质心个数,n_clusters

SSW(簇内):每个样本到自己质心的距离平方再加总,就是 99.1 的 SSE。越小,内聚越高。

SSB(簇间):每个质心到全局均值的距离平方,再按该簇人数 njn_j 加权后加总。越大,簇和簇离得越开、耦合越低。

前半截 SSB/SSW\mathrm{SSB}/\mathrm{SSW}:分子大、分母小,比值越大越好。后半截 (mk)/(k1)(m-k)/(k-1) 是在罚 kk:簇越多这项越小。不然 kk 一直加,SSW 跟着 SSE 往下掉,光看比值会偏向切得很碎。种类少也能把 CH 抬上去,所以「种类数少点更好」写进公式里了。

CH 越大越好。 k=1k=1 时分母 k1=0k-1=0,算不了,至少两簇。

3.3.2 代码实现
# 3. 定义函数,演示:CH轮廓系数法
def dm03_ch():
    # 1. 定义sse列表,记录:每个K值的 SSE值
    ch_list = []

    # 2. 生成数据集
    # 参1:样本数量  参2:特征数量  参3:4个簇  参4:4个簇的std标准差  参5:固定随机种子
    x, y = make_blobs(
        n_samples=1000, 
        n_features=2, 
        centers=[[-1, -1], [0, 0], [1, 1], [2, 2]], 
        cluster_std=[0.4, 0.2, 0.2, 0.2], 
        random_state=23
    )

    # 3. for循环遍历,获取到每个K值,计算其对应的SSE值,并添加到sse_list列表中
    for k in range(2, 100):  # 考虑簇外,至少2个簇
        # 3.1 创建KMeans对象,参1:聚的数量,参2:最大迭代次数,参3:固定随机种子
        estimator = KMeans(n_clusters=k, max_iter=100, random_state=23)
        # 3.2 模型训练
        estimator.fit(x)
        # 3.3 模型预测
        y_pre = estimator.predict(x)
        # 3.4 获取到每个簇的SSE值
        # sc_value = silhouette_score(x, y_pre)
        ch_value = calinski_harabasz_score(x, y_pre)
        # 3.5 将每个K值对应的sse值,添加到sse_list列表中
        ch_list.append(ch_value)

    # 4. 绘制sse曲线 -> 数据的可视化
    # print(f'ch_list列表:{ch_list}')
    # 4.1 创建画布,指定:画布的尺寸
    plt.figure(figsize=(20, 10))
    # 4.2 设置标题
    plt.title('CH value')
    # 4.3 设置x轴的刻度
    plt.xticks(range(1, 100, 3))
    # 4.4 添加x轴,y轴的标签
    plt.xlabel('K')
    plt.ylabel('CH')
    # 4.5 绘制网格
    plt.grid()
    # 4.6 绘制折线图
    # 参1:K值  参2:该K值对应的SSE值
    plt.plot(range(2, 100), ch_list)
    plt.show()
3.4 三种评估方法对比

SSE 只盯点到质心;SC 的 aa 是点到同簇其他人bb 把簇间也算进去;CH 三件事一起看(类内平方和小、类间平方和大、种类少点更好),距离走质心 / 全局均值,扫 kk 比 SC 快。

SSESCCH
看什么只簇内(内聚)簇内 aa + 簇间 bb簇内 SSW + 簇间 SSB + 罚 kk
距离相对谁样本 → 自己的质心样本 → 其他样本(同簇平均 / 最近邻簇平均)样本 → 质心(SSW=SSE);质心 → 全局均值(SSB)
好坏越小越好越大越好S[1,1]S\in[-1,1]越大越好
k=1k=1能算,通常最大不能算(没有「其他簇」)不能算(分母 k1=0k-1=0
k=nk=n(一人一簇)SSE=0=0,看起来最「好」aa 没定义,不会漂亮地掉到 0罚项 (mk)/(k1)(m-k)/(k-1) 把碎切压下去
怎么定 kk不能看绝对值最小,看肘拐点扫一圈 kk,取平均轮廓系数峰值扫一圈 kk,取 CH 峰值;算得快
sklearninertia_silhouette_scorecalinski_harabasz_score

SSE 必须配肘方法,否则 kk 越大越好是假象。要同时量化「挤不挤、离不离」,用 SC 或 CH;样本多、只为找 kk,CH 更省。

3.3 用户分群实现案例

"""
案例:
    基于用户的年收入和消费指数,根据用户的相似性进行聚类
"""
import os
os.environ['OMP_NUM_THREADS'] = '4'

# 导包
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
from sklearn.metrics import silhouette_score, calinski_harabasz_score
import pandas as pd
import numpy as np
from pathlib import Path

# 1. 定义函数,找聚类的质心数(K值)
def dm01_find_k():
    # 1. 加载数据集
    data_path = Path(__file__).parent / 'data' / 'customers.csv'
    df = pd.read_csv(data_path)
    # df.info()
    # print(df.head())

    # 2. 定义ssc_list,sc_list,记录:不同K值的 评估效果
    sse_list = []  # sse:只考虑簇内,越小越好
    sc_list = []   # sc:考虑簇内和簇外,越大越好

    # 抽取特征
    x = df.iloc[:, 3:5]
    print(x)

    # 3. 定义for训练,测试不同K值的评估效果
    for k in range(2, 20):
        # 3.1. 创建(KMeans模型)对象
        estimator = KMeans(n_clusters=k, max_iter=100, random_state=23)
        # 3.2. 模型训练
        estimator.fit(x)
        # 3.3. 模型预测
        y_pre = estimator.predict(x)
        # 3.4. 分别把评分添加到对应的列表中
        sse_list.append(estimator.inertia_)
        sc_list.append(silhouette_score(x, y_pre))

    # 4. 绘制折线图,看看K值 哪个最好
    plt.figure(figsize=(20, 10))
    # print(sse_list)
    # print(sc_list)
    plt.plot(range(2, 20), sse_list, label='SSE')
    plt.show()
    plt.figure(figsize=(20, 10))
    plt.plot(range(2, 20), sc_list, label='SC')
    plt.show()

    # 结论:K=5的时候,效果最好

# 2. 定义函数,实现:模型训练,模型预测,模型评估
def dm02_train_predict_evaluate():
    # 1. 加载数据集
    df = pd.read_csv(Path(__file__).parent / 'data' / 'customers.csv')
    # 2. 提取特征
    x = df.iloc[:, 3:5]
    # print(x.head())
    # 3. 模型训练 k=5 是刚才通过 SSE + 肘部法,SC轮廓系数 获取出来的
    estimator = KMeans(n_clusters=5, max_iter=100, random_state=23)
    estimator.fit(x)
    # 4. 模型预测
    y_pre = estimator.predict(x)
    # print(y_pre)  # [2, 3, 2, 3, 2, 0, 0, 1, 4, 4, 0, ...]
    # 5. 绘制5个簇的样本点 -> 散点图
    # print(x.values)  # [[15 39], [15 81], [16 6], [16 77], [17 40], [17 76], ...]
    plt.scatter(x.values[y_pre == 0, 0], x.values[y_pre == 0, 1])   # 0号簇
    plt.scatter(x.values[y_pre == 1, 0], x.values[y_pre == 1, 1])   # 1号簇
    plt.scatter(x.values[y_pre == 2, 0], x.values[y_pre == 2, 1])   # 2号簇
    plt.scatter(x.values[y_pre == 3, 0], x.values[y_pre == 3, 1])   # 3号簇
    plt.scatter(x.values[y_pre == 4, 0], x.values[y_pre == 4, 1])   # 4号簇

    # 6. 绘制5个簇的质心 -> 散点图
    # print(estimator.cluster_centers_)
    plt.scatter(estimator.cluster_centers_[:, 0], estimator.cluster_centers_[:, 1])
    # 7. 设置标题,x轴,y轴标签
    plt.title('Clusters of customers')
    plt.xlabel('Annual Income (k$)')
    plt.ylabel('Spending Score (1-100)')
    plt.legend()
    plt.show()

# 3. 测试
if __name__ == '__main__':
    # dm01_find_k()
    dm02_train_predict_evaluate()

    # 代码 x.values[y_pre == 0, 0] 解释
    x = [[15, 39], [15, 81], [16, 6], [16, 77], [17, 40], [17, 76], [18, 94], [18, 35], [19, 79], [19, 33]]
    # print(x[True, False, True, True, False, False])
    x2 = np.array(x)  # 模拟:x.values
    # print(type(x2))  # <class 'numpy.ndarray'>
    # 模拟:x.values[y_pre == 0]
    # 细节:随便写,个数要一致,即:True是要,False是不要
    result = x2[[True, False, True, True, False, False, True, False, True, True]]
    # print(result)  # [[15 39], [16 6], [16 77], [18 94], [19 79]]
    # 模拟:x.values[y_pre == 0, 0]
    result2 = x2[[True, False, True, True, False, False, True, False, True, True], 0]
    # print(result2)  # [15 16 16 18 19]
    # 模拟:x.values[y_pre == 0, 1]
    result3 = x2[[True, False, True, True, False, False, True, False, True, True], 1]
    # print(result3)  # [39  6 77 94 79]

4. 集成学习

4.1 Bagging - 随机森林

import pandas as pd
from sklearn.model_selection import train_test_split    # 切分训练集和测试集
from sklearn.tree import DecisionTreeClassifier         # 决策树
from sklearn.ensemble import RandomForestClassifier     # 随机森林算法(分类器)
from sklearn.model_selection import GridSearchCV        # 网格搜索
from pathlib import Path
path = Path(__file__).resolve().parent / 'data' / 'train.csv'
df = pd.read_csv(path)    # 加载数据
x = df[['Pclass', 'Sex', 'Age']].copy()  # 船舱等级,性别,年龄
y = df['Survived']
x['Age'] = x['Age'].fillna(x['Age'].mean())
x = pd.get_dummies(x, columns=['Sex'])
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23)
estimator3 = RandomForestClassifier()
params = {'n_estimators': [60, 90, 100, 130, 150], 'max_depth': [3, 5, 7, 9]}
gs_estimator = GridSearchCV(estimator3, param_grid=params, cv=3)
gs_estimator.fit(x_train, y_train)
y_pre3 = gs_estimator.predict(x_test)
print(f'网格搜索模型的准确率为:{gs_estimator.score(x_test, y_test)}')  # 0.7988826815642458
print(f'最佳参数:{gs_estimator.best_params_}')  # {'max_depth': 5, 'n_estimators': 100}

4.2 Boosting

4.2.1 AdaBoost自适应提升树

红酒品质分类案例

import pandas as pd
from sklearn.preprocessing import LabelEncoder        # 标签编码器
from sklearn.model_selection import train_test_split  # 训练集、测试集分割
from sklearn.tree import DecisionTreeClassifier       # 决策树模型
from sklearn.ensemble import AdaBoostClassifier       # AdaBoost分类器 -> 集成学习Boosting思想
from sklearn.metrics import accuracy_score            # 模型评估 -> 正确率
from pathlib import Path
path = Path(__file__).resolve().parent / 'data' / 'wine0501.csv'
df_wine = pd.read_csv(path)
df_wine = df_wine[df_wine['Class label'] != 1]
x = df_wine[['Alcohol', 'Hue']]  # 酒精 和 色泽
y = df_wine['Class label']       # 标签类
le = LabelEncoder()
y = le.fit_transform(y)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23, stratify=y)
estimator2 = AdaBoostClassifier(estimator=estimator1, n_estimators=200, learning_rate=0.1)
estimator2.fit(x_train, y_train)
y_pred2 = estimator2.predict(x_test)
print(f'AdaBoost模型预测结果:{y_pred2}')
print(f'AdaBoost模型准确率:{accuracy_score(y_test, y_pred2)}')  # 0.9583333333333334
4.2.2 GBDT

泰坦尼克号案例

import pandas as pd
from sklearn.model_selection import train_test_split        # 切割训练集和测试集
from sklearn.tree import DecisionTreeClassifier             # 决策树分类器
from sklearn.ensemble import GradientBoostingClassifier     # 梯度提升树分类器
from sklearn.metrics import accuracy_score, classification_report           # 模型评估
from sklearn.model_selection import GridSearchCV            # 网格搜索
from pathlib import Path
path = Path(__file__).resolve().parent / 'data' / 'train.csv'
df = pd.read_csv(path)
x = df[['Pclass', 'Sex', 'Age']].copy()
y = df['Survived']
x['Age'] = x['Age'].fillna(x['Age'].mean())
x = pd.get_dummies(x)
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23)
param_dict = {
    'n_estimators': [50, 60, 70, 80, 90, 100, 110],     # 弱学习器数量
    'learning_rate': [0.3, 0.5, 0.6, 0.7],              # 学习率
    'max_depth': [3, 5, 6, 7, 8, 9]                     # 树最大深度
}
estimator3 = GradientBoostingClassifier()
estimator4 = GridSearchCV(estimator3, param_dict, cv=5)
estimator4.fit(x_train, y_train)
print(f'网格搜索后的模型准确率为:{estimator4.best_score_}')  # 0.8005220131980696
print(f'网格搜索后的模型最佳参数为:{estimator4.best_params_}')  # {'learning_rate': 0.3, 'max_depth': 3, 'n_estimators': 50}
4.2.3 XGBoost

红酒品质分类案例

import joblib                                           # 保存和加载模型
import numpy as np                                      # 用于数值计算
import pandas as pd                                     # 用于数据处理
from sklearn.utils import class_weight                  # 计算样本权重
import xgboost as xgb                                   # 极限提升树对象
from collections import Counter                         # 统计数据
from sklearn.model_selection import GridSearchCV, train_test_split    # 训练集和测试集的划分
from sklearn.metrics import accuracy_score, classification_report       # 模型(分类)评估报告
from sklearn.model_selection import StratifiedKFold     # 分层K折交叉验证,类似于 网格搜索时 cv=折数
from pathlib import Path
def dm01_data_split():
    data_path = Path(__file__).parent / 'data' / '红酒品质分类.csv'
    df = pd.read_csv(data_path)
    x = df.iloc[:, :-1]
    y = df.iloc[:, -1] - 3  # 最后1列是标签,默认范围是:[3, 8] -> [0, 5]
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23, stratify=y)
    pd.concat([x_train, y_train], axis=1).to_csv(Path(__file__).parent / 'data' / '红酒品质分类_train.csv', index=False)  # 忽略索引
    pd.concat([x_test, y_test], axis=1).to_csv(Path(__file__).parent / 'data' / '红酒品质分类_test.csv', index=False)
def dm02_model_train():
    train_path = Path(__file__).parent / 'data' / '红酒品质分类_train.csv'
    test_path = Path(__file__).parent / 'data' / '红酒品质分类_test.csv'
    train_data = pd.read_csv(train_path)
    test_data = pd.read_csv(test_path)
    x_train = train_data.iloc[:, :-1]  # 除了最后1列,都是特征
    y_train = train_data.iloc[:, -1]   # 最后1列是标签
    x_test = test_data.iloc[:, :-1]
    y_test = test_data.iloc[:, -1]
    estimator = xgb.XGBClassifier(
        max_depth=5,                # 树的最大深度
        n_estimators=100,           # 树的数量
        learning_rate=0.1,          # 学习率
        random_state=23,            # 随机种子
        objective='multi:softmax',  # 多分类问题,使用多分类模型
    )
    cw = class_weight.compute_sample_weight('balanced', y_train)
    estimator.fit(x_train, y_train, sample_weight=cw)
    print(f'准确率:{estimator.score(x_test, y_test)}')
    joblib.dump(estimator, Path(__file__).parent / 'model' / '红酒品质分类.pkl')  # 后缀名也可以写 .pth,都是pickle文件格式
    print('模型保存成功!')
def dm03_use_model():
    train_path = Path(__file__).parent / 'data' / '红酒品质分类_train.csv'
    test_path = Path(__file__).parent / 'data' / '红酒品质分类_test.csv'
    train_data = pd.read_csv(train_path)
    test_data = pd.read_csv(test_path)
    x_train = train_data.iloc[:, :-1]  # 除了最后1列,都是特征
    y_train = train_data.iloc[:, -1]   # 最后1列是标签
    x_test = test_data.iloc[:, :-1]
    y_test = test_data.iloc[:, -1]
    estimator = joblib.load(Path(__file__).parent / 'model' / '红酒品质分类.pkl')
    param_dict = {
        'max_depth': [2, 3, 5, 6, 7],
        'n_estimators': [30, 50, 100, 150],
        'learning_rate': [0.2, 0.3, 1],
    }
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=23)
    gs_estimator = GridSearchCV(estimator, param_dict, cv=skf)
    gs_estimator.fit(x_train, y_train)
    y_pre = gs_estimator.predict(x_test)
    print(f'预测结果:{y_pre}')
    print(f'最优参数组合:{gs_estimator.best_params_}')  # {'learning_rate': 0.3, 'max_depth': 3, 'n_estimators': 150}
    print(f'最优估计器对象:{gs_estimator.best_estimator_}')
    print(f'最优得分:{gs_estimator.best_score_}')  # 0.6747579656862746
    print(f'准确率:{accuracy_score(y_test, y_pre)}')
if __name__ == '__main__':
    # dm01_data_split()
    # dm02_model_train()
    dm03_use_model()

5. 解决过拟合问题

5.1 L1正则化 - Lasso

def dm04_l1_regularization():
    np.random.seed(23)
    x = np.random.uniform(-3, 3, size=100)
    y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
    X = x.reshape(-1, 1)
    X3 = np.hstack([X, X ** 2, X ** 3, X ** 4, X ** 5, X ** 6, X ** 7, X ** 8, X ** 9])
    estimator = Lasso(alpha=0.1)  # alpha:正则化系数(惩罚系数),默认是1
    estimator.fit(X3, y)
    y_pre = estimator.predict(X3)
    print(f'均方误差:{mean_squared_error(y, y_pre)}')        # 参1:真实值,参2:预测值
    plt.scatter(x, y)                  # 以散点图的形式绘制 真实值
    plt.plot(np.sort(x), y_pre[np.argsort(x)], color='red')
    plt.show() 

5.2 L2正则化 - Ridge

def dm05_l2_regularization():
    np.random.seed(23)
    x = np.random.uniform(-3, 3, size=100)
    y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
    X = x.reshape(-1, 1)
    X3 = np.hstack([X, X ** 2, X ** 3, X ** 4, X ** 5, X ** 6, X ** 7, X ** 8, X ** 9])
    estimator = Ridge(alpha=10)
    estimator.fit(X3, y)
    y_pre = estimator.predict(X3)
    print(f'均方误差:{mean_squared_error(y, y_pre)}')        # 参1:真实值,参2:预测值
    plt.scatter(x, y)                  # 以散点图的形式绘制 真实值
    plt.plot(np.sort(x), y_pre[np.argsort(x)], color='red')
    plt.show() 

5.3 剪枝

L1 / L2 管线性模型的 (w);树过拟合是问得太细、把噪声也学进去。2.2.3 长完再砍,或长的时候就卡住。剪枝 = 把某棵子树收成一片叶子(分类用多数类,回归用均值),让树简单一点、泛化更好。

预剪枝后剪枝
何时还在长,切之前先问要不要这一刀先用训练集长成完整树,再自底向上收
判据这一刀验证集精度不升就停,当前节点当叶子这个非叶收成叶子后精度升了就剪
代价快,很多枝根本不长慢,整棵都要先长出来
风险可能切太早,欠拟合欠拟合风险小,通常留更多枝、泛化更好

sklearn 不是「每个节点拿验证集比精度」,后剪枝走 CART 的代价复杂度剪枝(CCP):

Rα(T)=R(T)+α×叶子数R_\alpha(T)=R(T)+\alpha\times\text{叶子数}

(\alpha) 越大砍得越狠。交叉验证选 (\alpha),就接近「验证集决定剪不剪」。XGBoost 的 (\gamma T) 是同一句话,不过税在选切点时就交,不只事后砍。

sklearn
预剪枝max_depthmin_samples_splitmin_samples_leafmin_impurity_decrease
后剪枝ccp_alpha;候选用 cost_complexity_pruning_path,再 GridSearchCV

泰坦尼克同一份数据(2.2.3.2 那套):

深度叶子训练测试
不剪211250.8830.804
预剪枝 max_depth=3380.8160.793
后剪枝 ccp_alpha≈0.00249160.8340.799

6. 时序预测问题

历史时间序列,去估后面还没发生的值。任务听着简单(过去 → 未来),做起来难:趋势会拐、噪声大、人还会改主意。

金融、交通、能源。都是「过去这条曲线,后面往哪拐」。

领域例子
金融股价、汇率、利率;企业资金流、机构流动性;GDP、CPI
交通流量、行驶速度、交通运行指数
能源见下表

能源里再拆发电侧和用电侧。火电出力相对稳,时序更常拿来对付会跟着天气抖的那部分:

预测什么
发电风电功率、光伏功率(风光随风、随日照变)
用电 / 市场工业和居民能源消费;电力负荷;电力市场价格、能源商品交易量

负荷预测问的是:这一小时、这一天,全社会要多少电。发少了停电,发多了浪费。后面案例会拿负荷这类序列上手。

6.1 时序数据分类

单步:只估未来 1 个时刻多步:一次估未来 多个 时刻
单变量:历史上就这一条序列用这一条的过去,估它自己的下一步用这一条的过去,估它后面一串
多变量:历史上有多条序列用这几条的过去,估目标的下一步用这几条的过去,估目标后面一串
任务变量步数做法
单变量单步11直接建一个模型,一个特征
单变量多步1多个方案 1:建一个模型,滚动——把刚测出来的值再塞回特征,去估再下一步
方案 2:不同时间步各建一个模型(专门估 t+1t+1、专门估 t+2t+2…)
多变量单步多个1直接建一个模型,多个特征
多变量多步多个多个方案 1:多特征模型 + 滚动(预测值再当特征)
方案 2:不同时间步分别建模(每个模型都吃多个特征)
方案 3:直接建模,一次吐出整段未来(深度学习里的序列到序列)

6.2 算法介绍

例子思路
统计简单平均、指数平滑、AR、MA、ARIMA简单平均最朴素:历史一平均,当未来的预测
深度学习RNN 一族、iTransformer自己吃序列;多步还可以序列到序列,对应 105.1 方案 3
传统机器学习线性回归、决策树、随机森林、XGBoost、LightGBM、SVM……凡是回归模型都行不直接吞一条线,要先把时序拆成二维表

传统机器学习和深度学习,落地时都常做同一件事:时序 → 一张二维结构化表,再拿回归模型去拟合。每一行是一个预测时刻,每一列是特征,最后一列是那天/那小时的负荷。XGBoost 吃的就是这种表,不是原始的 (time, value) 长条。

原始往往两张长表:时间和负荷;时间和天气(温度、湿度、风速)。拼成一行样本,时间本身要拆开,负荷的历史要做成特征。

时间能拆成什么(日历特征):

  1. 早上 / 中午 / 晚上
  2. 白天 / 夜里
  3. 季节、第几度(春夏秋冬、第几季度)
  4. 工作日 / 周末
  5. 年月日、时分秒里还能再挖节假日等

负荷能做成什么(滞后 / 窗口):

含义例子
近期设一个窗口,用刚过去的一段估下一段过去 1 小时 → 未来 1 小时用电量
远期 / 周期同一个钟点在更早周期的值昨天、上一周、上个月、上年同期的负荷

6.3 电力负荷预测案例

电力负荷预测/
├── data/     原始负荷 csv、做成的训练表
├── log/      跑起来写下的日志文件
├── model/    训好的 XGBoost(joblib / json 那种)
├── src/      正经业务:读数、特征、训练、预测
└── utils/    自己写的工具
  • utils/log.py
# -*- coding: utf-8 -*-
import logging
from pathlib import Path
# from datetime import datetime
import datetime
import pandas as pd

class Logger(object):
    # 日志级别关系映射
    level_relations = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warning': logging.WARNING,
        'error': logging.ERROR,
        'crit': logging.CRITICAL
    }

    def __init__(self, root_path, log_name, level='info', fmt='%(asctime)s - %(levelname)s: %(message)s'):
        # 相对路径按本文件所在目录解析,不跟进程 cwd 走
        root = Path(root_path)
        if not root.is_absolute():
            root = (Path(__file__).resolve().parent / root).resolve()
        self.root_path = root

        # 初始logger名称和格式
        self.log_name = log_name

        # 初始格式
        self.fmt = fmt

        # 先声明一个 Logger 对象
        self.logger = logging.getLogger(log_name)

        # 设置日志级别
        self.logger.setLevel(self.level_relations.get(level))

    def get_logger(self):
        # 指定对应的 Handler 为 FileHandler 对象, 这个可适用于多线程情况
        path = self.root_path / 'log'
        path.mkdir(parents=True, exist_ok=True)
        file_name = path / f'{self.log_name}.log'
        rotate_handler = logging.FileHandler(file_name, encoding="utf-8", mode="a")

        # Handler 对象 rotate_handler 的输出格式
        formatter = logging.Formatter(self.fmt)
        rotate_handler.setFormatter(formatter)

        # 将rotate_handler添加到Logger
        self.logger.addHandler(rotate_handler)

        return self.logger

if __name__ == '__main__':
    # 1. 创建日志对象
    # 参1:日志文件的父目录  参2:日志文件名
    logger = Logger('../', 'hg_test').get_logger()
    # 2. 往日志文件中写数据
    logger.info('我是普通的日志信息')   # 这个会被写到 hg.test 日志文件中
    logger.error('我是错误的日志信息')  # 日志默认是追加,不是覆盖
    # 3. 演示下错误日志,写入日志
    try:
        logger.info('开始计算了...')
        print(10 / 0)
    except Exception as e:
        logger.error(f'计算出错了,原因是: {e}')
    else:
        logger.info('计算成功!')
    finally:
        logger.info('计算结束!')
    # 4. 生成 train_年月日.log 日志文件
    print(datetime.datetime.now())  # 获取当前时间
    print(pd.to_datetime(datetime.datetime.now()).strftime("%Y%m%d%H%M%S"))
    new_name = 'train_' + pd.to_datetime(datetime.datetime.now()).strftime("%Y%m%d%H%M%S") + '.log'
    print(new_name)
  • utils/common.py
# -*- coding: utf-8 -*-
# 导包
import pandas as pd
import numpy as np
from pathlib import Path

# 该工具类的目的是:对数据做预处理 -> 时间格式化,按照时间升序排列,且对数据去重
# 数据集在 data 目录下的 train.csv 文件中 -> 拆分训练集 和 测试集
# 测试集在 data 目录下的 test.csv 文件中 -> 模拟项目上线后,真实的测试集

# 定义函数 data_preprocessing(),对数据做预处理操作

def data_preprocessing(path):
    """
    1.获取数据源
    2.时间格式化,转为2024-12-20 09:00:00这种格式
    3.按时间升序排序
    4.去重
    :param path:
    :return:
    """
    # 1. 加载数据集
    # data = pd.read_csv(Path(__file__).resolve().parent / 'data' / 'train.csv')
    # data.info()
    data = pd.read_csv(path)
    # 2. 时间格式化,转为: '%Y-%m-%d %H:%M:%S'
    # data['time'] = pd.to_datetime(data['time'], format='%Y-%m-%d %H:%M:%S')
    data['time'] = pd.to_datetime(data['time']).dt.strftime('%Y-%m-%d %H:%M:%S')
    # 3. 按时间升序排序
    data.sort_values('time', ascending=True, inplace=True)
    # 4. 去重
    data.drop_duplicates(inplace=True)
    # 5. 打印和返回
    # print(data)
    return data
  • src/train.py
# -*- coding: utf-8 -*-
# 导包
import sys
from pathlib import Path
import pandas as pd
import matplotlib.pyplot as plt
import datetime

# 无论从哪一层目录运行,都能找到项目根下的 utils
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from utils.log import Logger
from utils.common import data_preprocessing
from xgboost import XGBRegressor
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import mean_squared_error, mean_absolute_error
import joblib

plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['font.size'] = 15

# 1. 定义电力负荷模型类,配置日志,获取数据源
class PowerLoadModel(object):
    # 1.1 初始化属性信息
    def __init__(self, filename):
        # 1.1.1 拼接日志文件名
        logfile_name = "train_" + datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        # 1.1.2 创建日志对象
        self.logfile = Logger('../', logfile_name).get_logger()
        # 测试写一条日志
        self.logfile.info('开始创建 电力负荷模型类的 对象了')
        # 1.1.3 获取数据源
        self.data_source = data_preprocessing(filename)

# 4. 测试
if __name__ == '__main__':
    # 4.1 创建电力负荷模型类的对象
    input_file = PROJECT_ROOT / 'data' / 'train.csv'
    model = PowerLoadModel(input_file)
    # 4.2 打印数据源
    print(model.data_source)

机器学习

112. 电力负荷预测案例_查看数据整体和各小时负荷分布

# 2. 查看数据源的整体分布情况
def ana_data(data):  # analysis: 分析
    """
    1.查看数据整体情况
    2.负荷整体的分布情况
    3.各个小时的平均负荷趋势,看一下负荷在一天中的变化情况
    4.各个月份的平均负荷趋势,看一下负荷在一年中的变化情况
    5.工作日与周末的平均负荷情况,看一下工作日的负荷与周末的负荷是否有区别
    :param data: 数据源
    :return:
    """
    # 2.1 为了防止会修改源数据,我们做一次拷贝
    data = data.copy(deep=True)

    # 2.2 查看数据整体情况
    data.info()
    print(data.head())

    # 2.3 负荷整体的分布情况,直方图
    # 2.3.1 创建画布
    fig = plt.figure(figsize=(20, 40))
    # fig = plt.figure(figsize=(40, 80))
    # 2.3.2 添加子图
    ax1 = fig.add_subplot(411)
    ax1.hist(data['power_load'], bins=100)  # 负荷,直方图,100个区间
    ax1.set_title('负荷整体分布情况直方图')
    ax1.set_xlabel('负荷')
    # plt.show()

    # 2.4 各个小时的平均负荷趋势,看一下负荷在一天中的变化情况
    # 2.4.1 新增1列,充当小时
    data['hour'] = data['time'].str[11:13]
    # print(data.head())
    # 2.4.2 根据小时分组计算平均值
    data_hour_avg = data.groupby(by='hour', as_index=False)['power_load'].mean()
    # print(data_hour_avg)  # [列1 hour, 列2 power_load 当前小时的平均负荷]
    # 2.4.3 画出折线图
    ax2 = fig.add_subplot(412)
    ax2.plot(data_hour_avg['hour'], data_hour_avg['power_load'], color='b', linewidth=2)
    ax2.set_title('各小时的平均负荷趋势图')
    ax2.set_xlabel('小时')
    ax2.set_ylabel('负荷')

    fig_dir = PROJECT_ROOT / 'data' / 'fig'
    fig_dir.mkdir(parents=True, exist_ok=True)
    plt.savefig(fig_dir / '负荷整体的分布情况分析图.png')
    plt.show()

# 5. 测试
if __name__ == '__main__':
    # ...
    # 4.3 查看数据分布
    ana_data(model.data_source)

113. 电力负荷预测案例_按照月份_假日可视化数据

# 2. 查看数据源的整体分布情况
def ana_data(data):  # analysis: 分析
    # ...
    # 2.5 各个月份的平均负荷趋势,看一下负荷在一年中的变化情况
    data['month'] = data['time'].str[5:7]
    # print(data.head())
    data_month_avg = data.groupby('month', as_index=False)['power_load'].mean()
    ax3 = fig.add_subplot(413)
    ax3.plot(data_month_avg['month'], data_month_avg['power_load'], color='r', linewidth=2)  # x轴:月份,y轴:平均负荷
    ax3.set_title('各个月份的平均负荷趋势')
    ax3.set_xlabel('月份')
    ax3.set_ylabel('平均负荷')

    # 2.6 工作日与周末的平均负荷情况,看一下工作日的负荷与周末的负荷是否有区别
    data['week_day'] = data['time'].apply(lambda x: pd.to_datetime(x).weekday())
    # print(data.head())
    data['is_holiday'] = data['week_day'].apply(lambda x: 1 if x in [5, 6] else 0)  # 0:周一至周五,1:周末
    # print(data.head())
    power_load_holiday_avg = data[data['is_holiday'] == 1]['power_load'].mean()
    power_load_workday_avg = data[data['is_holiday'] == 0]['power_load'].mean()
    ax4 = fig.add_subplot(414)
    ax4.bar(x=['工作日平均负荷', '周末平均负荷'], height=[power_load_workday_avg, power_load_holiday_avg])
    ax4.set_title('工作日与周末的平均负荷对比')
    ax4.set_ylabel('平均负荷')
    # ...

114. 电力负荷预测案例_添加小时和月份字段

# 3. 特征工程(重点)
def feature_engineering(data, logger):
    """
    对给定的数据源,进行特征工程处理,提取出关键的特征
    1.提取出时间特征:小时、月份
    2.提取出相近时间窗口中的负荷特征:step大小窗口的负荷
    3.提取昨日同时刻负荷特征
    4.剔除出现空值的样本
    5.整理时间特征,并返回
    :param data: 数据源
    :param logger: 日志
    :return:
    """
    logger.info("===============开始进行特征工程处理===============")
    # 先拷贝数据
    result = data.copy(deep=True)
    logger.info("===============开始提取时间特征===================")
    # 3.1 提取出时间特征:小时、月份
    # 3.1.1 提取出对应的小时,用以表示短期的时间特征
    result['hour'] = result['time'].str[11:13]
    # 3.1.2提取出对应的月份,用以表示长期的时间特征
    result['month'] = result['time'].str[5:7]
    # 3.1.3 对时间特征hour和month字段进行one-hot编码处理
    hour_month_encoding = pd.get_dummies(result[['hour', 'month']])
    # print(hour_month_encoding.head())
    # print(hour_month_encoding.info())
    # 3.1.4 对one-hot编码后的结果进行拼接
    result = pd.concat([result, hour_month_encoding], axis=1)
    # print(result.head())
    print(result.info())

# 5. 测试
if __name__ == '__main__':
    # 4.1 创建电力负荷模型类的对象
    input_file = PROJECT_ROOT / 'data' / 'train.csv'
    model = PowerLoadModel(input_file)
    # 4.2 打印数据源
    # print(model.data_source)
    # 4.3 查看数据分布
    # ana_data(model.data_source)
    # 4.4 特征工程
    feature_engineering(model.data_source, model.logfile)

115. 电力负荷预测案例_特征工程_添加上n小时(窗口字段)

# 3. 特征工程(重点)
def feature_engineering(data, logger):
    # ...
    logger.info("==============开始提取相近时间窗口中的负荷特征====================")
    # 3.2 提取出相近时间窗口中的负荷特征:step大小窗口的负荷
    # print(result['step_load'])
    # tmp_df = result['step_load'].shift(1)
    # tmp_df = result['step_load'].shift(2)
    # print(tmp_df)
    # 指定window_size下的相近时间窗口负荷
    window_size = 3
    shift_list = [result['power_load'].shift(i) for i in range(1, window_size + 1)]
    shift_data = pd.concat(shift_list, axis=1)
    shift_data.columns = ['前' + str(i) + '小时' for i in range(1, window_size + 1)]
    result = pd.concat([result, shift_data], axis=1)

116. 电力负荷预测案例_特征工程_昨日同时刻负荷

# 3. 特征工程(重点)
def feature_engineering(data, logger):
    # ...
    logger.info("============开始提取昨日同时刻负荷特征===========================")
    # 3.3 提取昨日同时刻负荷特征
    # 3.3.1 给特征新增1列 yesterday_time
    result['yesterday_time'] = result['time'].apply(
        lambda x: (pd.to_datetime(x) - pd.to_timedelta('1d')).strftime('%Y-%m-%d %H:%M:%S'))
    # print(result.head())
    # 3.3.2 我们把所有的 日期 和 负荷拼接成字典,方便查找
    # 格式:{'2025-04-18 10:00:00': 100, '2025-04-18 11:00:00': 101, '2025-04-18 12:00:00': 102}
    time_load_dict = result.set_index('time')['power_load'].to_dict()
    # print(time_load_dict)
    # 3.3.3 新增1列 yesterday_load,表示:昨天的相同时刻的负荷
    result['yesterday_load'] = result['yesterday_time'].apply(lambda x: time_load_dict.get(x))
    # print(result.head())
    # result.info()

    # 3.4 剔除出现空值的样本
    result.dropna(inplace=True)

    # 3.5 整理时间特征列,并返回
    # 3.5.1 整理时间特征列
    time_feature_names = list(hour_month_encoding.columns) + list(shift_data.columns) + ['yesterday_load']
    logger.info(f"特征列名是:{time_feature_names}")  # ['hour_0', 'hour_1', 'hour_2', 'hour_3', 'hour_4', 'hour_5', 'hour_6', 'hour_7', 'hour_8', 'hour_9', 'hour_10', 'hour_11', 'hour_12', 'hour_13', 'hour_14', 'hour_15', 'hour_16', 'hour_17', 'hour_18', 'hour_19', 'hour_20', 'hour_21', 'hour_22', 'hour_23', 'month_0', 'month_1', 'month_2', 'month_3', 'month_4', 'month_5', 'month_6', 'month_7', 'month_8', 'month_9', 'month_10', 'month_11', '前1小时', '前2小时', '前3小时', 'yesterday_load']
    # 3.5.2 返回结果
    return result, time_feature_names

# 5. 测试
if __name__ == '__main__':
    # ...
    # 4.4 特征工程
    feature_data, feature_columns = feature_engineering(model.data_source, model.logfile)
    # 4.5 模型训练

    # print(pd.to_datetime('2025-04-18 10:00:00') - pd.to_timedelta('1d'))
    # print(pd.to_datetime('2025-04-18 10:00:00') - pd.to_timedelta('3d'))

117. 电力负荷预测案例_模型训练_评估_保存

# 4. 模型训练,评估,保存
def model_train(data, features, logger):
    """
    1.数据集切分
    2.网格化搜索与交叉验证
    3.模型实例化
    4.模型训练
    5.模型评价
    6.模型保存
    :param data: 特征工程处理后的输入数据
    :param features: 特征名称
    :param logger: 日志对象
    :return:
    """
    logger.info("=========开始模型训练===================")
    # 4.1 数据集切分
    x = data[features]
    y = data['power_load']
    # print(x.shape, y.shape)  # (14160, 40) (14160,)
    # print(x.head())
    # print(y.head())
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=23)

    # # 4.2 网格化搜索与交叉验证
    # logger.info('==========网格搜索 + 交叉验证 寻找最优超参==========')
    # logger.info(f'开始时间:{datetime.datetime.now()}')
    # # 4.2.1 定义参数字典
    # param_dict = {
    #     'n_estimators': [50, 100, 150, 200],
    #     'max_depth': [3, 5, 6, 7, 9],
    #     'learning_rate': [0.1, 0.01]
    # }
    # # 4.2.2 创建XGBoost模型对象(Extreme Gradient Boosting Tree,极端梯度提升树)
    # estimator = XGBRegressor()
    # grid_cv = GridSearchCV(estimator=estimator, param_grid=param_dict, cv=5)
    # # 4.2.3 模型训练
    # grid_cv.fit(x_train, y_train)
    # # 4.2.5 打印最优参数组合
    # logger.info(f'最优参数组合:{grid_cv.best_params_}')  # {'learning_rate': 0.1, 'max_depth': 5, 'n_estimators': 200}
    # logger.info(f'结束时间:{datetime.datetime.now()}')
    
    # 4.3 模型实例化
    estimator = XGBRegressor(n_estimators=200, max_depth=5, learning_rate=0.1)
    # 4.4 模型训练
    estimator.fit(x_train, y_train)
    y_pred = estimator.predict(x_test)
    # 4.5 模型评价
    print(f"均方误差:{mean_squared_error(y_test, y_pred)}")  # 5739.624491534932
    print(f"均方根误差:{root_mean_squared_error(y_test, y_pred)}")  # 75.76030947359529
    print(f"平均绝对误差:{mean_absolute_error(y_test, y_pred)}")  # 44.57857092100919
    print(f"平均绝对百分比误差:{mean_absolute_percentage_error(y_test, y_pred)}")  # 0.07295813995418061
    logger.info("=========================模型训练完成=============================")

    # 4.6 模型保存
    model_dir = PROJECT_ROOT / 'model'
    model_dir.mkdir(parents=True, exist_ok=True)
    joblib.dump(estimator, model_dir / 'xgb_20260922.pkl')  # pickle文件 -> 后缀名一般是.pkl,.pth,.pickle
    logger.info("模型保存完成,保存路径:{}".format(model_dir / 'xgb_20260922.pkl'))

    # 5. 测试
if __name__ == '__main__':
    # ...
    # 5.5 模型训练
    # 参1:处理后的全部数据集  参2:特征名称列表  参3:日志对象
    model_train(feature_data, feature_columns, model.logfile)
    # ...

118. 电力负荷预测案例_预测类代码实现

# -*- coding: utf-8 -*-
import os
import sys
from pathlib import Path
import pandas as pd
import datetime

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

from utils.log import Logger
from utils.common import data_preprocessing
from sklearn.metrics import mean_absolute_error
import matplotlib.ticker as mick
import joblib
import matplotlib.pyplot as plt


plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['font.size'] = 15

# 1. 配置电力负荷预测类
class PowerLoadPredict(object):
    def __init__(self, file_path):
        # 配置日志记录
        logfile_name = "predict_" + datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        self.logfile = Logger('../', logfile_name).get_logger()
        # 获取数据源
        self.data_source = data_preprocessing(file_path)
        # 历史数据转为字典,key:时间,value:负荷,目的是为了避免频繁操作dataframe,提高效率。实际开发场景中可以使用redis进行缓存
        # 格式为:{'2015-01-01 00:00:00': 100, '2015-01-01 01:00:00': 101, ...}
        self.data_dict = self.data_source.set_index('time')['power_load'].to_dict()

# 4. 测试
if __name__ == '__main__':
    """
    模型预测
    1.导包、配置绘图字体
    2.定义电力负荷预测类,配置日志,获取数据源、历史数据转为字典(避免频繁操作dataframe,提高效率)
    3.加载模型
    4.模型预测
        4.1 确定要预测的时间段(2015-08-01 00:00:00及以后的时间)
        4.2 为了模拟实际场景的预测,把要预测的时间以及以后的负荷都掩盖掉,因此新建一个数据字典,只保存预测时间以前的数据字典
        4.3 预测负荷
            4.3.1 解析特征(定义解析特征方法)
            4.3.2 利用加载的模型预测
        4.4 保存预测时间对应的真实负荷
        4.5 结果保存到evaluate_list,三个元素分别是预测时间、真实负荷、预测负荷,方便后续进行预测结果评价
        4.6 循环结束后,evaluate_list转为DataFrame
    5.预测结果评价
        5.1 计算预测结果与真实结果的MAE
        5.2 绘制折线图(预测时间-真实负荷折线图,预测时间-预测负荷折线图),查看预测效果 
    """
    # 4.1 创建电力负荷预测类对象
    input_file = PROJECT_ROOT / 'data' / 'test.csv'
    pp = PowerLoadPredict(input_file)
    print(pp.data_source)
    print(pp.data_dict)
    # 4.2 加载模型对象
    estimator = joblib.load(PROJECT_ROOT / 'model' / 'xgb_20260922.pkl')

119. 电力负荷预测案例_解析预测特征

# 2. 预测数据解析特征,保持与模型训练时的特征列名一致
def pred_feature_extract(data_dict, time, logger):
    """
    预测数据解析特征,保持与模型训练时的特征列名一致
    1.解析时间特征
    2.解析时间窗口特征
    3.解析昨日同时刻特征
    :param data_dict:历史数据,字典格式,key:时间,value:负荷
    :param time:预测时间,字符串类型,格式为2024-12-20 09:00:00
    :param logger:日志对象
    :return:
    """
    logger.info(f'=========解析预测时间为:{time}所对应的特征==============')
    # 特征列清单
    feature_names = ['hour_00', 'hour_01', 'hour_02', 'hour_03', 'hour_04', 'hour_05',
                     'hour_06', 'hour_07', 'hour_08', 'hour_09', 'hour_10', 'hour_11',
                     'hour_12', 'hour_13', 'hour_14', 'hour_15', 'hour_16', 'hour_17',
                     'hour_18', 'hour_19', 'hour_20', 'hour_21', 'hour_22', 'hour_23',
                     'month_01', 'month_02', 'month_03', 'month_04', 'month_05', 'month_06',
                     'month_07', 'month_08', 'month_09', 'month_10', 'month_11', 'month_12',
                     '前1小时', '前2小时', '前3小时', 'yesterday_load']
                     
    # 2.1 解析时间特征,即:time字段(预测时间)对应的 那条数据样本
    # 2.1.1 截取要预测的time字段的小时信息
    pred_hour = time[11:13]  # 例如:'2015-08-01 00:00:00' -> '00'
    hour_part = []
    for i in range(24):
        if pred_hour == feature_names[i][5:7]:
            hour_part.append(1)
        else:
            hour_part.append(0)
    # print(hour_part)
    # 2.1.2 截取要预测的time字段的月份信息
    pred_month = time[5:7]  # 例如:'2015-08-01 00:00:00' -> '08'
    month_part = []
    for i in range(24, 36):
        if pred_month == feature_names[i][6:8]:
            month_part.append(1)
        else:
            month_part.append(0)
    # print(month_part)

    # 2.2 解析时间窗口特征
    his_part = []
    # 2.2.1 前1小时的负荷
    last_1h_time = (pd.to_datetime(time) - pd.to_timedelta('1h')).strftime('%Y-%m-%d %H:%M:%S')
    # print(last_1h_time)
    last_1h_load = data_dict.get(last_1h_time, 500)  # 获取前一个小时对应的负荷,若没有则默认为500
    # print(last_1h_load)
    # 2.2.2 前2小时负荷
    last_2h_time = (pd.to_datetime(time) - pd.to_timedelta('2h')).strftime('%Y-%m-%d %H:%M:%S')
    last_2h_load = data_dict.get(last_2h_time, 500)
    # 2.2.3 前3小时负荷
    last_3h_time = (pd.to_datetime(time) - pd.to_timedelta('3h')).strftime('%Y-%m-%d %H:%M:%S')
    last_3h_load = data_dict.get(last_3h_time, 500)

    # 2.3 昨日同一时刻的负荷
    yesterday_time = (pd.to_datetime(time) - pd.to_timedelta('1d')).strftime('%Y-%m-%d %H:%M:%S')
    yesterday_load = data_dict.get(yesterday_time, 500)

    # 2.4 拼接特征数据
    feature_data = hour_part + month_part + [last_1h_load, last_2h_load, last_3h_load, yesterday_load]
    # print(feature_data)
    # print(type(feature_data))
    
    # 2.5 转成df对象,返回
    feature_df = pd.DataFrame(feature_data, columns=feature_names)
    # print(feature_df)
    return feature_df

# 4. 测试
if __name__ == '__main__':
    # ...
    # 4.3 确定要预测的时间段:2015-08-01 00:00:00及以后的时间
    # print(pp.data_source[pp.data_source['time'] >= '2015-08-01 00:00:00'])
    pred_times = pp.data_source['time'][pp.data_source['time'] >= '2015-08-01 00:00:00']
    # 4.4 为了模拟实际场景的预测,把要预测的时间以及以后的负荷都掩盖掉,因此新建一个数据字典,只保存预测时间以前的数据字典
    for pred_time in pred_times:  # pred_time: 2015-08-01 00:00:00, 2015-08-01 01:00:00, ...
        print(f"正在预测{pred_time}的负荷")
        data_dict_masked = {k: v for k, v in pp.data_dict.items() if k < pred_time}
        # print(data_dict_masked)
        # 4.5 预测负荷
        # 4.5.1 解析特征(定义解析特征方法)
        feature_df = pred_feature_extract(data_dict_masked, pred_time, pp.logfile)
        # 4.5.2 利用加载的模型预测
        y_pred = estimator.predict(feature_df)
        print(f"预测结果为:{y_pred}")

120. 电力负荷预测案例_结果展示

# 3. 绘制时间与预测负荷折线图,时间与真实负荷折线图
def prediction_plot(data):
    """
    绘制时间与预测负荷折线图,时间与真实负荷折线图,展示预测效果
    :param data: 数据一共有三列:时间、真实值、预测值
    :return:
    """
    # 3.1 创建画布
    fig = plt.figure(figsize=(40, 26))
    # 3.2 创建子图
    ax = fig.add_subplot()
    # 3.3 绘制时间与真实负荷的折线图
    ax.plot(data['预测时间'], data['真实负荷'], color='blue', label='真实值')
    ax.plot(data['预测时间'], data['预测负荷'], color='red', label='预测值')
    # 3.4 添加标题
    ax.set_title('真实负荷和预测负荷关系图')
    ax.set_xlabel('时间')
    ax.set_ylabel('负荷')
    # 3.5 添加网格
    ax.grid(True, linestyle='--', alpha=0.5)
    # 3.6 添加图例
    ax.legend(loc='best', fontsize=20)
    # 3.7 设置刻度间隔,以及x轴标签值 旋转角度
    # 横坐标时间若不处理太过密集,这里调大时间展示的间隔
    ax.xaxis.set_major_locator(mick.MultipleLocator(50))
    # 时间展示时旋转45度
    plt.xticks(rotation=45)  # x轴标签值 旋转角度
    # 3.8 保存图片
    save_path = PROJECT_ROOT / 'data' / 'fig'
    plt.savefig(save_path / '真实负荷和预测负荷关系图.png')
    # 3.9 展示图片
    plt.show()

# 4. 测试
if __name__ == '__main__':
    # ...
    # 4.8 循环结束后,evaluate_list转为DataFrame
    evaluate_df = pd.DataFrame(evaluate_list, columns=['预测时间', '真实负荷', '预测负荷'])
    # print(evaluate_df)
    # 4.9.预测结果评价
    # 4.9.1 计算预测结果与真实结果的MAE
    print(f'平均绝对误差:{mean_absolute_error(evaluate_df['真实负荷'], evaluate_df['预测负荷'])}')
    # 4.9.2 绘制折线图(预测时间-真实负荷折线图,预测时间-预测负荷折线图),查看预测效果
    prediction_plot(evaluate_df)

121. 电力负荷预测案例_扩展思路

本案例已经能跑通:csv → 拆二维表 → XGBoost → 估下一小时。还可以从四头再拧。

121.1 特征

现在主要是小时 / 月份独热、前 3 小时、昨天同期。还能:

做什么
时间窗口window_size=3 不一定最优,加长、缩短、或再加「上周同一小时」
历史负荷滚动均值、峰值、工作日/周末分开的滞后,比光塞三个点更有结构
外部特征温度、湿度、风速;工业用电、居民用电分开的历史。这才是 6.1 严格意义上的多变量

负荷跟天气绑得很死,夏天空调一开曲线就翘。数据源里现在只有时间和负荷,精度往上抬,外部特征往往比再调两个 XGBoost 参数更值钱。

121.2 算法

先分群再建模按地区,或用 3.1 把负荷曲线相近的点先抱团,每团单独一棵树。全国一个模型容易两头不讨好
换模型6.2 里的 LightGBM、RNN、iTransformer。表格式继续 Boosting;真要序列到序列再上深度
组合4.1 Bagging 多套子模型投票;Stacking 用另一层模型去揉几路预测

不是必须上 Transformer。窗口和天气没备齐之前,换算法经常涨不动。

121.3 预测速度

预测时也要拼特征:前 1/2/3 小时、昨天同期。现在 data_dict 整表塞内存,predict.py 里已经写了可以换成 Redis:历史负荷按时间当 key,查一次就有,不用每次扫 DataFrame。线上每小时一批点时,缓存比反复读 csv 重要。

121.4 工程扩展

数据源现在只读 csv。工具层接到 MySQL、Postgres、Oracle、ES、Redis、Hive,换库不改训练逻辑
部署项目打成容器,换台机器不用重配 Anaconda 路径
服务化预测从脚本里拆出来,对外 HTTP:Web 传来时间(和地区),返回下一小时负荷。FastAPI 那套正好接

前两头管准不准,后两头管能不能上线。课上停在 csv + 本地 pkl,这四条是同一条项目往外长的方向。

深度学习

机器学习刚收住:sklearn 拆表、fit、评估。大模型开发要的底座换成 多层神经网络,框架换成 PyTorch。这条课卡在学习路线里「机器学习 → 深度学习 → NLP → …」的第二段。

1. 深度学习与神经网络课程导学

1.1 这门课在整条路线上干什么

就业「王炸」:金融、医疗、客服、教育、新闻、娱乐、自动驾驶、制造、电商、物流都在上大模型。岗位名一长串——Agent / RAG / Dify / Coze / 微调 / 多模态 / NLP / 推理优化

课上把大模型能力拆成四段,本课只打中间「技术基建」里的深度学习这块

阶段内容你现在
基础能力Python、数据分析、计基、大模型概念已经走过
技术基建机器学习、PyTorch 与深度学习、NLP、Transformer机器学习刚完,从这里进
核心工程微调、RAG、知识图谱、Agent后面
前沿多模态、RLHF、压缩蒸馏、部署更后面

所以不是上来微调 GPT,而是先把网络怎么堆、误差怎么回传、CNN / RNN 各吃什么数据搞清楚。

1.2 神经网络怎么长到今天

节点
1943MP 神经元模型
1949Hebb 学习规则
1958Perceptron,第一次兴起
1969第一次 AI 寒冬
1982Hopfield
1986BP 反向传播,第二次兴起
1995SVM 把风头抢走一阵
2006DBN,深度网络重新被认真对待
2012CNN 第三次兴起
2020 前后图上标了 Winter?——大模型起来之后叙事又换了

课上要走的是这条线里还能用手推的部分:神经元 → 一层 → BP 多层 → CNN / RNN。

1.3 从生物神经元到一个公式

生物神经元:胞体收信号,树突接收,轴突传出,突触接到下一个细胞,神经递质过缝。脉冲有方向。

抽象成一层人工神经元,和线性回归几乎同一句话:

z=iwixi+b,y=f(z)z=\sum_i w_i x_i+b,\qquad y=f(z)
生物人工
树突上的输入xix_i
突触强度权重 wiw_i
胞体整合加权和 +b+b
是否发放激活函数 ff
轴突输出yy

机器学习里 y=wx+by=wx+b 没有 ff,或者逻辑回归把 ff 换成 sigmoid。深度学习把很多个这样的单元叠起来,ff 负责弯,叠深了才能自动抠特征。

1.4 一层、BP、多层

正向:输入层 → 隐含层 → 输出层,信息往右走。

BP(反向传播):输出和真值比出误差,再从右往左把误差分给每个 ww(图上 WjkW_{jk}WijW_{ij})。和线性回归梯度下降同一类事,只是层多了,要用链式法则一层层传。1986 年 BP 能算了,多层才训得动。

再深就是多个 Hidden Layer:输入若干维,中间几层全连接,最后输出。层数上去,叫深度学习。

深度学习是机器学习的一个分支,核心是用多层神经网络做自动特征提取和模式识别。sklearn 那套要你自己做标准化、独热、窗口;深度网络希望从原始像素、原始序列里自己学表示。

1.5 CNN 和 RNN:这门课两条主干

吃什么结构直觉
CNN 卷积图像卷积抽局部、降采样缩小、再全连接分类。例:32×32×3 图 → 若干卷积/池化 → 展平 → 10 类
RNN 循环序列(文本、语音、时间序列隐藏状态 sts_t 带着上一拍 st1s_{t-1},按时间展开:xt1,xt,xt+1x_{t-1},x_t,x_{t+1} 共用一套 U,W,VU,W,V

负荷预测那次用 XGBoost 拆表,是传统机器学习吃时序。121.2 说的 RNN / iTransformer,就是序列不拆死、让网络自己记前后。图像课上还没碰过,CNN 是新的一条。

1.6 还是那三块:数据、算法、算力

和机器学习导学同一张图:

角色
数据基础。文字、图像、视频。没有量,多层网喂不饱
算法引擎。机器学习是落地引擎;深度学习主攻 NLP / CV 这类高维
算力平台。CPU 也能跑小网;深度、大模型靠 GPU

层一深,矩阵乘爆炸,所以后面会碰到 PyTorch 把计算丢到 GPU。

1.7 课程学什么、学完能干什么

顺序:

PyTorch 框架 → 深度学习基础(BP 等)→ CNN → RNN → 案例实战

课上说的四块亮点,对应到作业形态:

落到什么
PyTorch 从会用到懂原理张量、点乘、自动求导;手拆反向,报错能对上哪一层
人工神经网络基础损失(MSE、交叉熵)、梯度下降、L1/L2;电商价格分类那种小案例
CNN + RNNCIFAR10 图像分类;词嵌入 + RNN 做文本(歌词生成那种)
项目贯穿入门:PyTorch 线性回归 / 逻辑回归;进阶:图像分类、文本生成

目标四条:神经网络基础、用 PyTorch 搭结构、会 CNN、会 RNN。这是大模型应用的底座,不是大模型本身。

2. 知识框架介绍

1.7 那条顺序,换成自下而上四层。机器学习在图像和文本上偏弱,所以从人工神经网络再分出 CNN(CV)和 RNN(NLP);两条线最后都接到全连接层

                    机器学习吃图 / 文本偏弱
                   /                        \
        4 CNN(CV)                          4 RNN(NLP)
        卷积 → 池化 → 全连接                  词嵌入 → 循环 → 全连接
        案例:CIFAR10 图像分类                案例:歌词生成
                   \                        /
                    3 全连接层(两条共用)
                              |
                    2 ANN 人工神经网络
                    输入 1 层 → 隐藏 n 层 → 输出 1 层
                              |
                    1 张量 Tensor(标量、向量都是它的特例)
                    PyTorch / TensorFlow
学什么对应后面
1 张量标量、向量、矩阵 → 统一叫 Tensor。框架先会 PyTorch(课上也点名 TensorFlow,主线用前者)建网之前的数据形态
2 ANN1.4 那套:一层输入、若干隐藏、一层输出;BP 回传线性回归 / 逻辑回归用 PyTorch 重写
3 全连接每个输出都连上一层所有输入。CNN、RNN 抽出特征之后,分类 / 生成都靠它收口两条主干的共同尾巴
4 CNN卷积抽局部、池化缩小,再全连接。专吃图像CIFAR10,走向 CV
4 RNN词先嵌入成向量,循环层记前后,再全连接。专吃序列歌词生成,走向 NLP

先把 1、2 垒实:张量算错、层没叠对,后面卷积核、循环状态都会漂。CNN / RNN 不是另起炉灶,是 ANN 换了中间几层,尾巴仍是全连接。

3. 深度学习_简介

课件两张图把位置先钉死,再对比「人抽特征」和「网自己抽」。1.4 已经说过它是机器学习的分支;这里按课上原话把三层关系和黑盒说清楚。

3.1 它在哪一层

人工智能 AI          造能干活的机器(仿智)
    └── 机器学习 ML   不必把规则一条条写死;特征工程主要是人做
            └── 深度学习 DL   特征工程交给算法;底层是深度神经网络
  • 机器学习是实现人工智能的一种途径,不是唯一途径
  • 深度学习是机器学习的一个子集,也就是实现机器学习的一种方法
  • 更具体:深度学习是机器学习里一种基于对数据做特征学习的算法

时间轴从 1950s 画到 2010s,和 1.2 那张年表是同一条故事:感知机 → 寒冬 → BP → 2012 CNN 再热。

网络本身:1.3 一个神经元 = 加权求和 + 激活函数。叠起来就是输入层 → 若干隐藏层 → 输出层。「深度」指的是中间用了多层;每一层做一次非线性变换,逐渐抽出更复杂、更抽象的特征。没有非线性,多层等价于一层,深不起来。

3.2 和传统机器学习差在哪

同一件事:图里一辆车,输出「是车 / 不是车」。

传统机器学习深度学习
输入图 / 原始数据一样,张量里只是数值
中间人做特征提取,再交给分类器特征提取 + 分类都在网里,合成一块黑盒
谁设计特征依赖人工算法从大量数据里自动抽

传统路线:输入 → 人工特征(边、轮子、颜色……)→ 分类 → 输出。深度路线:输入直接进多层网,中间不拆给你看。这就是叫黑盒、可解释性差的原因——不是不能用,是你很难指着某一层说「这就是轮子」。

它尤其擅长高维数据:图像、语音(频谱图)、文本。课上对应两条:1.5CNN(图)和 RNN(序列 / 频谱 / 文本)。2 里两条线最后都接到全连接,只是中间换了抽特征的层。

4. 深度学习_特点

3 把位置和黑盒说完。课上把特点收成四条,前两条是它强在哪,后两条是代价。

特点一句话
多层非线性变换一层一层弯,浅层抓边和颜色,深层才认出物体、脸
自动特征提取不用人做特征工程,从原始数据里自己学有用的表示
要大数据和算力标注要多,训练常靠 GPU;图、NLP 的突破是数据和算力堆出来的
可解释性差内部不透明,叫黑箱;为什么判成这一类,不好指给人看

4.1 多层非线性变换

模型由多个层次组成,每一层都用非线性激活把输入拧一下。3.1 说了:没有非线性,多层叠起来还是一层线性。浅层通常抓住简单特征(边缘、颜色),更高层再组合成复杂模式(物体、人脸)。「深」不是层数好看,是一层层抽象往上走。

4.2 自动特征提取

3.2 同一件事。传统机器学习:人设计特征再分类。深度学习:从原始像素、波形、字里自己学有用的特征,少做手工特征工程。图像、语音、文本这些高维领域,它才显得出色。

4.3 大数据和计算能力

多层参数极多,小数据容易过拟合,还算不动。通常要大量标注强计算(GPU)。1.6 那三块里,数据和算力在这里被点名:图像识别、NLP 近几年的突破,是大数据 + 高性能计算托起来的,不只是换了个算法名字。

4.4 可解释性差

内部运作相对不透明,叫黑箱。知道输入输出,很难说明「为什么判成车」。3.2 的黑盒就是这条。医疗、金融这类要给说法的场景,这是挑战,不是小毛病。

5. 常用模型介绍

2 课上只深挖 CNN、RNN;这页把常见模型摊开,先认名字和各自吃什么。

输入层 → 隐藏层(卷积 / 池化 / 全连接等)→ 输出层
模型英文课上怎么用
卷积神经网络CNN图像,CV。隐藏层常见:卷积、池化、全连接
循环神经网络RNN文本,NLP
自编码器Autoencoder无监督:降维、特征、异常检测
生成对抗网络GAN生成:图像、视频
TransformerTransformerNLP:翻译、生成;丢掉递推,改注意力
深度强化学习DRL点名,这门课不展开
图神经网络GNN点名,这门课不展开

5.1 CNN

专吃图像:分类、检测、分割。1.5 那条。卷积层自动抽局部特征,池化把图缩小、参数变少,算得动。抽完再全连接收口。

5.2 RNN

专吃序列:NLP、语音。有记忆,能看输入在时间上的依赖(上一拍的状态带着走)。标准 RNN 记不了太远,长期依赖很难;后面 LSTM / GRU、再往后 Transformer,都是在补这一刀。

5.3 自编码器

无监督。编码器把输入压成低维表示,解码器再从这点低维往回重建输入。常用于降维、学特征、异常检测(重建差得大,多半是异常)。

5.4 GAN

两个子网对着练:生成器造假样本,尽量像真的;判别器分辨真假。用在图像生成、视频合成。

5.5 Transformer

主战场是 NLP:机器翻译、文本生成、摘要。丢掉 RNN 那种一步步递推,改用自注意力(self-attention),整句可以并行看。这就是现在大模型底座;课上先把 CNN / RNN 垒实,名字先挂上。

6. 应用场景介绍

深度学习是底座,上面叠计算机视觉、文本处理、语音交互、人机交互、知识图谱这些能力,再落到智慧医疗、智能安防、智能零售、智慧教育、智能家居、智能制造、智能汽车、智能机器人等产业。

常见场景:计算机视觉、自然语言处理、自动驾驶、推荐系统、医疗健康、工业与制造、语音与音频。下面把 CV、NLP、推荐 写细,最后补 多模态大模型:图、文、语音不再各管各的。

6.1 计算机视觉(CV)

对应 5.1。把像素变成「这张图里有什么、在哪」。

任务干什么例子
图像分类整图归到某一类人脸识别、物体检测的前置;社交媒体自动打标签、医学影像里找病变
目标检测图像或视频里定位并分类多个对象自动驾驶行人检测、监控里的入侵检测
面部识别用脸做身份验证或分类手机解锁、安防监控
图像生成按输入造新图风格转换、超分辨率;艺术风格迁移、老照片修复(5.4

6.2 自然语言处理(NLP)

对应 5.25.5。文本、语音都算序列。

任务干什么例子
机器翻译一种语言自动翻成另一种Google 翻译、实时语音翻译
情感分析正面 / 负面 / 中性社交媒体监控、商品评论
文本生成生成通顺文本写作助手、新闻生成
语音识别语音 → 文字Siri、Alexa、自动字幕
聊天机器人理解输入并回话客服机器人、GPT 类虚拟助手

6.3 推荐系统

按历史行为猜下一步爱看什么、爱买什么。

场景依据例子
影音评分、播放记录Netflix、Spotify
电商购买、浏览亚马逊、淘宝
社交互动、关注Facebook、Instagram 内容 / 好友推荐

自动驾驶、医疗、工业、智能家居,是把 CV、语音嵌进具体行业,不是另起一套算法。

6.4 多模态大模型

6.16.3 默认一种输入对一种输出:图进图出、字进字出。多模态是把几种信号放进同一个模型:一张图 + 一句话一起理解,或听语音再对着画面回答。

底座多半是 5.5,再接上图像 / 语音编码器。常见用法:

任务干什么例子
图文理解看图答题、看图说话视觉问答、图片描述、文档里的表和字一起读
文生图 / 图生文一种模态生成另一种文生图、给图写标题
语音 + 文本 / 画面听、说、看打通实时翻译时对着画面指物体、带视觉的语音助手

前面三条(CV、NLP、推荐)是专才;多模态大模型是把专才拼成一个能同时看、读、听的通用接口。后面动手仍先落在单模态的 CV(CIFAR10)和 NLP(文本生成),这里先知道它把谁拼在一起。

7. 发展史介绍

1.2 按年份点过节点。这里拆成两层:人工智能四段浪潮,再收成深度学习自己的四段。反向传播单独画清楚——多层能训,靠的就是它。

7.1 人工智能四段

阶段时间靠什么节点
符号主义1950s–1970s专家系统,规则由人写1950 图灵国际象棋程序;1962 Samuel 跳棋程序赢过人类高手(第一次浪潮)
统计主义1980s–2000s统计模型1993 Vapnik 提出 SVM;1997 IBM 深蓝赢卡斯帕罗夫(第二次浪潮)
神经网络 / 深度学习21 世纪初深度网络2012 AlexNet;2016 AlphaGo 赢李世石(第三次浪潮)
大规模预训练2017–至今大模型、生成2017 Transformer;2018 BERT、GPT;2022 ChatGPT,进入 AIGC

前两段几乎不靠「深」网络:符号是 if-then,统计是 SVM 这类浅模型。第三次浪潮才把 5.1 推上台面;2017 之后叙事换成预训练大模型,接 5.56.4

7.2 深度学习四段

阶段时间发生了什么
早期探索1940s–1980sMcCulloch–Pitts 神经元(仿生物);1957 Rosenblatt 感知机,能做简单二分类;1960s 多层感知机(MLP)出现,算力和数据不够,用不开
挑战与瓶颈1980s–1990s1986 Rumelhart 等人发表 反向传播(BP),多层可以用梯度下降改参数。算力弱、图像和语音又高维,深度网还是没铺开
复兴与突破2000s–2010s2006 Hinton 深度信念网络(DBN),深度学习重新被当回事;2012 Krizhevsky 的 AlexNet 在 ImageNet 上大赢,CNN 成主流
爆发期2016–至今2016 AlphaGo;2017 Transformer;2018 BERT / GPT;2022 ChatGPT → 大模型 AIGC

7.1 对上:BP 解决了「多层怎么训」,但还要等数据和 GPU;AlexNet 是计算机视觉爆点,Transformer 是 NLP / 大模型爆点。

7.3 反向传播(BP)

网络从左到右:

输入层 → 隐藏层(若干层全连接)→ 输出层

每个神经元仍是 1.3 那句:

y=wx+by = wx + b

训练走两趟:

正向:输入往右算到输出,和真值比,得到损失
反向:损失从输出层往左传,逐层更新权重 w、偏置 b
方向干什么
正向(→)输入层进数,隐藏层一层层 wx+bwx+b 再激活,输出层吐预测
损失预测和标签差多少
反向(←)按损失对每个 ww 求梯度,从右往左改参数

和线性回归的梯度下降是一类事,只是层多了,要用链式法则一层层拆。1986 年 BP 能算之后,7.2 里「多层」才不是纸上结构。没有这一步,深网只是一堆固定的 ww,学不成特征。

8. PyTorch框架简介

2 最底层是张量,动手用的框架是 PyTorch(同一层还有 TensorFlow,后面代码走前者)。

8.1 它是什么

PyTorch 是基于 Python 的深度学习框架,把数据封成**张量(Tensor)**再算。提供构建、训练、部署机器学习和深度学习模型的工具,学术和工业都在用,尤其是计算机视觉、自然语言处理、强化学习。

安装(清华源):

pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple

8.2 特点

特点含义
类似 NumPy 的张量计算Tensor 像 ndarray,多了梯度、能上 GPU
自动微分正向算出损失,反向自动给每个参数梯度,不用手推 7.3
深度学习库nn.Linear、卷积、RNN 这类层直接调
动态计算图边跑边建图,Python 控制流(if / for)就是网络结构
GPU 加速CUDA,矩阵乘丢显卡,对上 4.3
多场景、跨平台CV / NLP / 强化学习;Windows、Linux、云端都能跑

自动微分怎么接到参数更新。例如 y=2x2y=2x^2x=10x=10,对 xx 求导:

dydx=4x=40\frac{dy}{dx}=4x=40

权重按梯度下降改一刀(学习率 0.010.01,当前 w=10w=10):

w=w学习率×梯度=100.01×40=9.6w_{\text{新}}=w_{\text{旧}}-\text{学习率}\times\text{梯度}=10-0.01\times 40=9.6

PyTorch 的 autograd 干的就是「求这个 40」;你只写正向的 y=2x2y=2x^2,反向不用自己推 4x4x

8.3 发展简史

节点
2016Facebook 发布第一版
20181.0,进入生产可用

再往前,深度学习框架走过几段:早期 C / C++ 库(OpenNN 一类)API 重、没 GPU;2012 前后 Caffe、Theano、Chainer 起来,有 GPU、能搭复杂网;2015–2018 TensorFlow、Keras、MXNet、Caffe2 和 PyTorch 挤在稳定期,自动微分和分布式成标配;2019 之后 TensorFlow 2.x 和 PyTorch 并列为常用两套,国产还有飞桨等。

现在写模型常见就是 nn.Linear(3, 5) 这种一层:3 个输入、5 个输出的全连接。后面张量运算、自动求导、把网丢到 GPU,都在这个框架里完成。