彩笔运维勇闯机器学习--多元线性回归(实战)

前言

书接上文,上一小节简单介绍了多元回归的基本原理、使用方式,本小节来实践:qps与cpu、内存、磁盘io、网络io之间的关系

获取数据

参考一元线性回归的获取方式

复制代码
from flow import *
from datetime import datetime

start_time = datetime.strptime('2025-04-06 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
end_time = datetime.strptime('2025-04-06 23:59:59', '%Y-%m-%d %H:%M:%S').timestamp()
step = 600
sls_step = 3600*6

query = get_query_data(start_time, end_time, sls_step)
cpu = get_cpu_data(start_time, end_time, step)
memory = get_memory_data(start_time, end_time, step)
network_in = get_network_in_data(start_time, end_time, step)
network_out = get_network_out_data(start_time, end_time, step)
file_read = get_file_read_data(start_time, end_time, step)
file_write = get_file_write_data(start_time, end_time, step)

print('cpu 数据个数为{} ,前10数据为{}'.format(len(cpu), cpu[:10]))
print('query 数据个数为{} ,前10数据为{}'.format(len(query), query[:10]))
print('memory 数据个数为{} ,前10数据为{}'.format(len(memory), memory[:10]))
print('network_in 数据个数为{} ,前10数据为{}'.format(len(network_in), network_in[:10]))
print('network_out 数据个数为{} ,前10数据为{}'.format(len(network_out), network_out[:10]))
print('file_read 数据个数为{} ,前10数据为{}'.format(len(file_read), file_read[:10]))
print('file_write 数据个数为{} ,前10数据为{}'.format(len(file_write), file_write[:10]))

脚本!启动:

特征标准化

特征数据已经获取完成,看起来是没问题,但是仔细分析,好像又有点问题,首先cpu数据非常小,内存数据又很大,特征数据之间的数量级差距太大了,特别是在多元回归中,不同特征的量纲和尺度可能差异巨大。若未标准化, 回归系数的数值大小会受特征尺度影响,导致难以直接比较特征的重要性

那首先先人为的进行数据缩放

复制代码
query = [round(x/10000, 4) for x in get_query_data(start_time, end_time, sls_step)]
cpu = get_cpu_data(start_time, end_time, step)
memory = [round(x/1024/1024/1024, 4) for x in get_memory_data(start_time, end_time, step)]
network_in = [round(x/1024/1024, 4) for x in get_network_in_data(start_time, end_time, step)]
network_out = [round(x/1024/1024, 4) for x in get_network_out_data(start_time, end_time, step)]
file_read = [round(x/1024/1024, 4) for x in get_file_read_data(start_time, end_time, step)]
file_write = [round(x/1024, 4) for x in get_file_write_data(start_time, end_time, step)]

看看效果

调整过后,特征数据都是2位数的了,只不过单位不一样

  • query的单位是万
  • memory的单位是G
  • network_in的单位是M
  • network_out的单位是M
  • file_read的单位是K
  • file_write的单位是K

再进行数据标准化,数据标准化的公式

\[z = \frac{x - \mu}{\sigma} \]

  • μ 是特征的均值,
  • σ 是特征的标准差。

标准化后,所有特征的尺度统一,均值为0,标准差为1

复制代码
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

训练模型

多元回归!启动

复制代码
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
from flow import *
from datetime import datetime
import pandas as pd

def adjusted_r2(r2, n, p):
    return 1 - (1 - r2) * (n - 1) / (n - p - 1)

start_time = datetime.strptime('2025-04-06 00:00:00', '%Y-%m-%d %H:%M:%S').timestamp()
end_time = datetime.strptime('2025-04-06 23:59:59', '%Y-%m-%d %H:%M:%S').timestamp()
step = 600
sls_step = 3600*6

query = [round(x/10000, 4) for x in get_query_data(start_time, end_time, sls_step)]
cpu = get_cpu_data(start_time, end_time, step)
memory = [round(x/1024/1024/1024, 4) for x in get_memory_data(start_time, end_time, step)]
network_in = [round(x/1024/1024, 4) for x in get_network_in_data(start_time, end_time, step)]
network_out = [round(x/1024/1024, 4) for x in get_network_out_data(start_time, end_time, step)]
file_read = [round(x/1024/1024, 4) for x in get_file_read_data(start_time, end_time, step)]
file_write = [round(x/1024, 4) for x in get_file_write_data(start_time, end_time, step)]

features = {
    'feature1': cpu,
    'feature2': memory,
    'feature3': network_in,
    'feature4': network_out,
    'feature5': file_read,
    'feature6': file_write,
}


data = {
    'result': query,
}

data.update(features)

df = pd.DataFrame(data)

X = df[[
    'feature1',
    'feature2',
    'feature3',
    'feature4',
    'feature5',
    'feature6',
]]
y = df['result']

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = LinearRegression()
model.fit(X, y)

y_pred = model.predict(X)
MSE = mean_squared_error(y, y_pred)
r2 = r2_score(y, y_pred)
print("R²:", r2)
print("MSE:", MSE)

p = len(features.keys())
n = len(data['result'])
adjusted_r2 = adjusted_r2(r2, n, p)
print(f"调整决定系数 (Adjusted R²): {adjusted_r2}")

脚本!启动:

完美的模型,来得太顺利反而有点不太习惯了

通过lasso回归来看下哪一些参数是强相关的

复制代码
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.1)
lasso.fit(X_scaled, y)

for i, coef in enumerate(lasso.coef_, 1):
    print(f'x{i} 的回归系数:{coef:.4f}')

lasso回归检查特征

脚本!启动:

这里面已经有一些特征在划水了,找出来,把他们裁掉!当然lasso回归已经自动帮我们把假装干活的特征自动裁员了(打工人,哭死=_=!),但是还是有必要找出原因来的

将这4个特征画个图看一下

X2(内存)、X5(file_read)、X6(file_write)符合预期:特征是一条直线,不随着query波动

  • 首先看X2,这是内存。在业务服务中,由于是java服务,内存在启动的时候划分了一大块交给jvm管理,所以在操作系统看来,变化不大,要折腾都在jvm内部折腾。所以memory是一条直线
  • X5,这是file_read。在业务服务中,几乎没有的file_read,都是通过network_read,所以file_read一条直线
  • X6,这是file_write。在业务服务中,都是缓存写,再由操作系统同步到磁盘,所以file_write是由操作系统决定的,近似一条直线

再看X3,这是network_in,按理说这个特征是与query强相关的,有明显的数据波动,但是lasso回归的时候还是把它略掉了,认为它是一个多余的特征

t检验

定义

检验每个自变量是否真正影响了结果。更直接一点,揪出谁在工作谁在划水!

实践

先介绍一个专门用于数据分析的工具:statsmodels,用来做统计推断,并且提供额外的模型检查工具

安装也很简单:

复制代码
pip3 install statsmodels

添加代码:

复制代码
import statsmodels.api as sm

X_with_const = sm.add_constant(X)
sm_model = sm.OLS(y, X_with_const).fit()
print(sm_model.summary())

脚本!启动:

这怎么还越整越复杂了!这输出的都是些什么东西啊?!

别着急,这部分主要是描述t检验,先拿出t检验相关的数据

简单解释一下:

  • 第一列就是参数,const是常数项,也就是公式中的\(β_0\),其余的是6个参数
  • 第二列coef,是所谓的系数,就是\(β_1 β_2 \dots β_n\)
  • 第三列std err,就是所谓的标准误差
  • 第四列t值,计算公式为:

\[t=\frac{coef}{std err} \]

  • 第五列P>|t|,所谓的P值,计算公式就不列出来了,我自己都没搞明白 =_=!,只要记住非常有用就行,一会会用

  • 最后两列一起看[0.025 0.975],这是所谓的置信区间,什么置信区间?正态分布熟悉吧,记住置信区间和正态分布强相关就行了,

    复制代码
    feature1      31.2856      9.573      3.268      0.001      12.356      50.215

    这里意味着,有95%的系数是落在[12.36, 50.21] 之间

说了这么半天,最直接有效的就是看P值,越接近0,那就说明该系数越相关

还有个更简单的方法,直接丢gpt看看

ai牛逼,科技提高工作效率。至此,通过t检验,也可以发现有哪些特征是强相关的,哪些特征是无用的。feature1(cpu)、feature3(network_in)、feature4(network_out)这3个特征对于结果有重大的影响。之前的lasso回 归中,feature3(network_in)已经被判定了对于结果没有重大影响

为什么这两个评估工具给出了不一样的答案呢?

多重共线性

回归模型中两个或多个预测变量(自变量)之间存在高度相关性的情况。针对于我们的这个模型,很容易联想到feature3(network_in)、feature4(network_out),是高度相关性的

VIF(方差膨胀因子)

复制代码
from statsmodels.stats.outliers_influence import variance_inflation_factor

vif_df = pd.DataFrame()
vif_df['features'] = X.columns
vif_df['VIF'] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
print(vif_df)

VIF > 10,说明存在严重的多重共线性

相关系数矩阵

复制代码
corr_matrix = df.corr()
print(corr_matrix)

直接丢进gpt

小结

根据上述分析,query可以直接通过cpu特征就可以分析出来了,其他的特征存在与否,其实并不重要

在statsmodels的报告中已经揭示了我们的模型存在严重的共线性:

Cond. No.这个值大于1000的时候,就表示存在了严重的多重共线性问题

多重共线性带来的问题:

  • 回归系数估计变得不稳定,小的数据变化可能导致系数大幅变化
  • 系数标准误增大,t统计量减小,导致变量可能显得不显著
  • 难以区分单个变量对因变量的独立影响
  • 虽然预测可能仍然准确,但解释单个变量的影响变得困难

ridge(岭回归)

数学模型

\[\mathcal{L} = \frac{1}{2n} \sum_{i=1}^{n} (y_i - \hat{y}i)^2 + \lambda \sum{j=1}^{p} β_j^2 \]

实践

复制代码
from sklearn.linear_model import Ridge

ridge = Ridge(alpha=1.0)
ridge.fit(X_scaled, y)
y_ridge = ridge.predict(X_scaled)
r2_ridge = r2_score(y, y_ridge)
r2_adj_ridge = adjusted_r2(r2_ridge, n, 7)

for i, coef in enumerate(ridge.coef_, 1):
    print(f'x{i} 的回归系数:{coef:.4f}')

print("\nridge模型:")
print(f"R² = {r2_ridge:.4f}, Adjusted R² = {r2_adj_ridge:.4f}")

ridge回归中,所有系数都会被压缩向零但不会完全为零,所以不像lasso回归中直接为0那么直观:

  • 绝对值较大的系数通常对应更重要的特征
  • 正负号表示特征与目标变量的正/负相关关系

如图所示,X1与X4是更为重要的特征,这与lasso回归得出的结论是一样的

总结

多元回归的复杂性,是由于特征参数过多带来的新的问题,无用特征、多重共线性,要找出特征的权重,要用到一些检验方法,比如t检验、VIF、相关矩阵系数等。如果只想关注模型泛化能力,可以通过lasso、ridge等回归来自动筛选特征

综上所述,本次多元回归之旅最终的结果又回到了一元回归,饶了一大圈又回到了原点,但是并非毫无收获,除了收获了一大堆模型评估方法,什么调整决定系数、t检验、VIF等,还有一堆陌生的检测算法,lasso回归、ridge回归等。并且提供了今后对于多元回归的一些方法论

装杯时刻

那位兄弟问了,你这洋洋洒洒搞了这么半天,怎么装杯呢?

今天这个就不给老板汇报了,毕竟如果只关注结果的话,又回到了一元回归的方法论来,但是依然有价值分享给同事,毕竟这一堆猛烈的方法输出,每月一次的团队培训内部培训,这不是又有题材了不是吗?

联系我

  • 联系我,做深入的交流

至此,本文结束

在下才疏学浅,有撒汤漏水的,请各位不吝赐教...