海上飞龙 發表於 2025-9-1 10:15:00

彩笔运维勇闯机器学习--拟合

<h2 id="前言">前言</h2>
<p>今天我们来讨论拟合的问题</p>
<p>在之前的篇幅,主要讨论的是线性回归的问题,不管是一元、多元、多项式,本质都是线性回归问题。线性回归在机器学习中属于“监督学习”,也就是使用已有的、预定义的“训练数据”集合,训练系统,在解释未知数据时,也能够很好的解释</p>
<p>而模型训练完成之后,可能会有3中状态:“欠拟合”、“最佳适配”、“过拟合”。本小节就来消息讨论一下,怎么判断训练出来的模型处于什么样的状态</p>
<h2 id="过拟合">过拟合</h2>
<p>老规矩,先运行起来,再探索原理</p>
<pre><code>import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score

np.random.seed(0)
X = np.linspace(0, 1, 30)
y_true = np.sin(2 * np.pi * X)
y = y_true + np.random.normal(0, 0.2, X.shape)

X_train, X_test, y_train, y_test = train_test_split(X.reshape(-1, 1), y, test_size=0.3)

degree = 10
model = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('line', LinearRegression())
])
model.fit(X_train, y_train)

y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

mse_train = mean_squared_error(y_train, y_train_pred)
r2_train = r2_score(y_train, y_train_pred)
mse_test = mean_squared_error(y_test, y_test_pred)
r2_test = r2_score(y_test, y_test_pred)

print(f"训练集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"验证集 MSE: {mse_test:.4f} ,R²:{r2_test}")

</code></pre>
<p>数据是由sin函数加上一些噪点组成的,按照37比例分成训练集与测试集。而模型则是最高阶为10的多项式</p>
<p>脚本!启动:<br>
<img alt="watermarked-fit_regression_1_1" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100300926-131195422.png" class="lazyload"></p>
<p>在训练数据上表现不错,但是在测试数据上表现就不行了,误差明显上升,调整系数R²也下降了,这就是所谓的过拟合现象</p>
<h2 id="交叉验证">交叉验证</h2>
<p>从上面看到,将训练数据手动划分为两部分,训练集与测试集,通过测试集,就发现了模型的过拟合现象。那将训练数据多次划分,并且重复训练与验证,就能有更大的概率提前发现模型过拟合情况。当然,手动做这个工作耗时耗力,而本小节要讨论的交叉验证就是为了完成这个工作的</p>
<h4 id="留出法">留出法</h4>
<p>这在之前的演示中已经给出来了,就是主动划分训练集与测试集,通过random_state来决定每次划分的集合不同</p>
<ul>
<li>优点:简单易用</li>
<li>缺点:结果受单次划分影响大,尤其在小数据集中波动性高</li>
</ul>
<pre><code>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
</code></pre>
<h4 id="k折交叉验证">k折交叉验证</h4>
<p>将数据集均等分为k个子集(k通常取5或10),依次选取第i个子集作为验证集,其余k-1个子集作为训练集,重复k次,每次计算模型性能指标(如准确率、F1值等),最终结果为k次验证的平均值。本质上就是多次计算去平均值</p>
<ul>
<li>优点:降低数据划分的随机性,结果更稳定</li>
<li>缺点:计算成本较高(需训练k次模型)</li>
</ul>
<pre><code>kf = KFold(n_splits=5, shuffle=True, random_state=0)
</code></pre>
<pre><code>from sklearn.model_selection import KFold, cross_val_score

kf = KFold(n_splits=5, shuffle=True, random_state=0)

neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores
print("5折MSE:{}".format(np.round(mse_scores, 2)))
print("平均MSE:{} \n".format(round(np.mean(mse_scores), 2)))

r2_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=kf, scoring="r2")
print("5折R²分数:{}".format(r2_scores))
print("平均R²:{}\n".format(r2_scores.mean()))

</code></pre>
<p>这里也可以使用<code>cross_validate</code>获取多个指标</p>
<p>脚本!启动:</p>
<p><img alt="watermarked-fit_regression_1_2" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100312811-84423415.png" class="lazyload"></p>
<p>由于k折交叉验证非常常用,可以适应大部分情况,这里给出第二种写法,灵活使用</p>
<pre><code>mse_list = []
r2_list = []

for train_index, test_index in kf.split(X):
    X_train, X_test = X, X
    y_train, y_test = y, y

    model.fit(X_train.reshape(-1, 1), y_train.reshape(-1, 1))
    y_pred = model.predict(X_test.reshape(-1, 1))

    mse = mean_squared_error(y_test.reshape(-1, 1), y_pred)
    r2 = r2_score(y_test.reshape(-1, 1), y_pred)

    mse_list.append(mse)
    r2_list.append(r2)

print("5折MSE:{}".format(np.round(mse_list, 2)))
print("平均MSE:{} \n".format(round(np.mean(mse_list), 2)))

print("5折R²分数:{}".format(np.round(r2_list, 2)))
print("平均R²:{}\n".format(round(np.mean(r2_list), 4)))
</code></pre>
<p>第二种写法更是解释了,k折交叉验证本质就是自动划分训练集与测试集,然后再去进行模型训练</p>
<p>综上所述,在某个定义域内(0~1),10阶多项式去解释sin函数(加入噪点),平均mse是0.25,平均R²是0.56,模型泛化能力是非常差的</p>
<p>最后补充一点:</p>
<p>n_splits这个参数不但控制了折数,还控制了训练集与测试集的比例,比如<code>n_splits=5</code>,每次用 4/5 的数据做训练集,1/5 做测试集;比如<code>n_splits=3</code>,每次用 2/3 的数据做训练集,1/3 做测试集</p>
<h4 id="留一交叉验证">留一交叉验证</h4>
<p>k折交叉是按照比例,按照折数(通常5折或10折),对训练集与测试集进行“比例”划分,由<code>n_splits</code>参数控制。而留一交叉每次只会选择1个样本作为测试机,其余的为训练集,然后遍历整个样本进行训练</p>
<p>举个例子,如果样本数为</p>
<table>
<thead>
<tr>
<th>训练集</th>
<th>测试集</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<p>如果样本量很小的情况,那留一交叉验证就非常适合,因为每个样本都被用作验证集一次,不浪费任何一个数据点。但是一旦样本数量变多,那训练的速度就会非常慢</p>
<pre><code>from sklearn.model_selection import LeaveOneOut, cross_val_score

loo = LeaveOneOut()
neg_mse_scores = cross_val_score(model, X.reshape(-1, 1), y, cv=loo, scoring='neg_mean_squared_error')
mse_scores = -neg_mse_scores

print("平均MSE:{} \n".format(round(np.mean(mse_scores), 2)))
</code></pre>
<p><img alt="watermarked-fit_regression_1_3" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100324643-1916338214.png" class="lazyload"></p>
<h4 id="小结">小结</h4>
<p>还有2个常用的分层k折交叉验证、时间序列交叉验证,这里做一个对比,就不展开细说了</p>
<table>
<thead>
<tr>
<th>方法</th>
<th>适用场景</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>留出法</td>
<td>大数据集快速验证</td>
<td>计算快</td>
<td>结果受单次划分影响大</td>
</tr>
<tr>
<td>k折交叉验证</td>
<td>通用场景</td>
<td>结果稳健</td>
<td>计算成本中等</td>
</tr>
<tr>
<td>留一法(LOO)</td>
<td>极小数据集</td>
<td>无偏差</td>
<td>计算成本极高</td>
</tr>
<tr>
<td>分层k折</td>
<td>类别不平衡数据</td>
<td>保持类别分布</td>
<td>仅适用于分类问题</td>
</tr>
<tr>
<td>时间序列CV</td>
<td>时间相关数据</td>
<td>防止未来信息泄露</td>
<td>必须按时间顺序划分</td>
</tr>
</tbody>
</table>
<h2 id="学习曲线">学习曲线</h2>
<p>通过训练误差与测试误差,来判断模型是否过拟合:</p>
<ul>
<li>欠拟合:训练误差和验证误差都很高,模型太简单</li>
<li>过拟合:训练误差很低,但验证误差很高,模型太复杂</li>
<li>恰到好处:训练误差和验证误差都低,并且两者接近</li>
</ul>
<pre><code>train_sizes, train_scores, valid_scores = learning_curve(
    model, X_train, y_train, cv=5, n_jobs=-1,
    train_sizes=np.linspace(0.1, 1.0, 10)
)

train_scores_mean = np.mean(train_scores, axis=1)
valid_scores_mean = np.mean(valid_scores, axis=1)

plt.figure()
plt.plot(train_sizes, train_scores_mean, 'o-', color='r', label='Training score')
plt.plot(train_sizes, valid_scores_mean, 'o-', color='g', label='Validation score')

plt.xlabel('Training examples')
plt.ylabel('Score')
plt.title('Learning Curve')
plt.legend(loc='best')
plt.grid(True)
plt.show()

</code></pre>
<p>脚本!启动:</p>
<p><img alt="watermarked-fit_regression_1_4" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100333287-1326874473.png" class="lazyload"></p>
<p>这图一看就不正常,我们丢进gpt,让它帮我们分析一下</p>
<p><img alt="watermarked-fit_regression_1_5" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100352021-848306322.png" class="lazyload"></p>
<p><img alt="watermarked-fit_regression_1_6" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100339560-943277214.png" class="lazyload"></p>
<h2 id="验证曲线">验证曲线</h2>
<p>用来评估模型性能与某个超参数之间关系的一种可视化工具。而所谓的超参数,则是模型中必须要设置的参数,比如多项式中的阶数degree、lasso|ridge中的alpha等等</p>
<pre><code>from sklearn.model_selection import validation_curve
from sklearn.model_selection import train_test_split

param_range = np.arange(1, 15)
train_scores, valid_scores = validation_curve(
    model, X.reshape(-1, 1), y,
    param_name='poly__degree',
    param_range=param_range,
    cv=5,
    scoring='r2'
)

train_mean = np.mean(train_scores, axis=1)
valid_mean = np.mean(valid_scores, axis=1)

plt.figure(figsize=(8, 5))
plt.plot(param_range, train_mean, label='Training Score', marker='o', color='r')
plt.plot(param_range, valid_mean, label='Validation Score', marker='o', color='g')
plt.xlabel('Polynomial Degree')
plt.ylabel('R² Score')
plt.legend(loc='best')
plt.grid(True)
plt.xticks(param_range)
plt.show()

</code></pre>
<p>脚本!启动:</p>
<p><img alt="watermarked-fit_regression_1_7" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100407226-973186525.png" class="lazyload"></p>
<p>懒了,直接丢ai!</p>
<p><img alt="watermarked-fit_regression_1_8" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100415134-1642604341.png" class="lazyload"></p>
<p><img alt="watermarked-fit_regression_1_9" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100420687-2115222956.png" class="lazyload"></p>
<p><img alt="watermarked-fit_regression_1_10" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100433940-1476446688.png" class="lazyload"></p>
<h2 id="正则化">正则化</h2>
<p>所谓的正则化,就是:</p>
<ul>
<li>L1 正则化(Lasso 回归)
<ul>
<li>在损失函数中添加模型参数的绝对值之和</li>
<li>特点:倾向于将某些参数压缩到 0,从而实现特征选择</li>
</ul>
</li>
<li>L2 正则化(Ridge 回归)
<ul>
<li>在损失函数中添加模型参数的平方和</li>
<li>特点:倾向于将参数值缩小,但不会完全压缩到 0</li>
</ul>
</li>
<li>弹性网络(Elastic Net)
<ul>
<li>结合 L1 和 L2 正则化</li>
</ul>
</li>
</ul>
<p>lasso与ridge我们之前在线性回归的时候用过,用来降低无用特征的对结果的影响,而lasso与ridge也可以抑制高阶项系数</p>
<p>用lasso来测试一下</p>
<pre><code>from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LassoCV

lassoCV = LassoCV(alphas=np.logspace(-4, 0, 20), cv=5, max_iter=1000000)
lasso = Pipeline([
    ('poly', PolynomialFeatures(degree=degree)),
    ('scaler', StandardScaler()),
    ('lasso', lassoCV)
])

lasso.fit(X_train, y_train)
lasso_train_pred = lasso.predict(X_train)
lasso_test_pred = lasso.predict(X_test)

mse_train = mean_squared_error(y_train, lasso_train_pred)
r2_train = r2_score(y_train, lasso_train_pred)
mse_test = mean_squared_error(y_test, lasso_test_pred)
r2_test = r2_score(y_test, lasso_test_pred)

print('===='*20)
print('lasso:\n')
print(f"训练集 MSE: {mse_train:.4f} ,R²:{r2_train}")
print(f"验证集 MSE: {mse_test:.4f} ,R²:{r2_test}")
</code></pre>
<p>lasso与之前的使用方式不同,使用了LassoCV,新方式可以自动选择alpha,并且会尝试所有的alpha可能值,再加上交叉验证,使得lasso回归的结果达到最佳状态</p>
<p>脚本!启动:</p>
<p><img alt="watermarked-fit_regression_1_11" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100441884-2073400847.png" class="lazyload"></p>
<p>通过正则化L1,也就是lasso回归,能够答复提高模型的泛化能力,其实和之前线性回归去掉无用特征一样,在高阶多项式中,lasso回归一样能够去掉无用的阶数,保留真正影响结果的阶数</p>
<pre><code>print('lasso回归系数')
print(lasso.named_steps['lasso'].coef_)
</code></pre>
<p><img alt="watermarked-fit_regression_1_12" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202509/1416773-20250901100447984-1160001409.png" class="lazyload"></p>
<p>由此可见,lasso删除了,0、3、4、7、8、9阶,保留了1、2、5、6、10阶</p>
<h2 id="超参数与普通参数">超参数与普通参数</h2>
<ul>
<li>普通参数是自己学习到的,比如线性回归中的回归系数、截距</li>
<li>超参数是模型训练之前就要设置的,比如多项式的阶数degree</li>
</ul>
<p>由于超参数无法通过模型自己去学习,所以需要通过多种方法去尝试、调优,而超参数调优的是一件非常非常复杂的工作,涉及到多种不同模型,有很多不同的方法。这里我们看的是单个超参数(比如多项式的阶数),目的也很简单,就是通过不同超参数的表现,查看过拟合的情况</p>
<p>列一下一些常见的超参数,而对应的模型在今后的文章中多少都会涉及到</p>
<table>
<thead>
<tr>
<th>模型</th>
<th>超参数</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>线性回归(带正则)</td>
<td>alpha(Ridge/Lasso)</td>
<td>正则化强度</td>
</tr>
<tr>
<td>决策树</td>
<td>max_depth</td>
<td>树的最大深度</td>
</tr>
<tr>
<td>K 近邻</td>
<td>n_neighbors</td>
<td>邻居个数</td>
</tr>
<tr>
<td>SVM</td>
<td>C、gamma</td>
<td>惩罚系数 / 核函数参数</td>
</tr>
<tr>
<td>多项式回归</td>
<td>degree</td>
<td>多项式的阶数</td>
</tr>
<tr>
<td>神经网络</td>
<td>learning_rate、batch_size、epochs</td>
<td>学习速率 / 批量大小 / 训练轮数</td>
</tr>
</tbody>
</table>
<h2 id="小结-1">小结</h2>
<p>本文通过一个过拟合的例子,使用不同的方法,交叉验证、学习曲线、正则化等方法验证了怎么去评估模型过拟合</p>
<h2 id="联系我">联系我</h2>
<ul>
<li>联系我,做深入的交流</li>
</ul>
<p><img alt="" width="500" height="200" loading="lazy" src="https://img2024.cnblogs.com/blog/1416773/202411/1416773-20241121135740959-1907948957.png#" class="lazyload"></p>
<hr>
<p>至此,本文结束<br>
在下才疏学浅,有撒汤漏水的,请各位不吝赐教...</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>本文来自博客园,作者:it排球君,转载请注明原文链接:https://www.cnblogs.com/MrVolleyball/p/19067789</p>
<div>本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。 </div><br><br>
来源:https://www.cnblogs.com/MrVolleyball/p/19067789
頁: [1]
查看完整版本: 彩笔运维勇闯机器学习--拟合