Logistic回归实战:从原理到部署的完整指南
1. 从能不能预测到概率有多大分类问题的本质转变很多人第一次接触机器学习分类任务时脑子里想的都是给我一个答案——这封邮件是不是垃圾邮件这个用户会不会流失这个病人有没有患病但真正做过几个项目之后你会发现实际业务场景里决策者要的往往不是一个非黑即白的结论而是一个概率。因为有了概率才能做成本收益的权衡把正常邮件误判为垃圾邮件的代价和漏掉一封垃圾邮件的代价完全不是一个量级。Logistic回归模型Logistic Regression恰恰就是解决这类问题的经典工具。它名字里带回归干的却是分类的活这一点让不少初学者困惑。我第一次看到这个模型的时候也纳闷明明是个分类器为什么叫回归后来才想明白它本质上是在用线性回归的思路去拟合一个事件发生的对数几率输出的是一个0到1之间的概率值然后再通过阈值把它变成类别判断。这篇文章适合谁看如果你正在学机器学习基础或者工作中遇到了二分类问题需要一个可解释性强、训练速度快、部署成本低的方案那Logistic回归大概率是你的第一选择。它不像深度学习那样需要大量数据和算力也不像树模型那样调参空间巨大但它胜在逻辑清晰、可解释、稳定。很多金融风控、医疗辅助诊断、营销响应预测的场景至今仍然把Logistic回归作为基线模型甚至主力模型。我写这篇内容的出发点很简单网上讲Logistic回归的文章很多但大部分要么只推公式不讲直觉要么只调包不讲原理要么给个demo就完事。我想从一个实际使用者的角度把从理解原理到落地部署的完整链路讲清楚包括那些文档里不会写、只有踩过坑才知道的细节。2. 拆开Logistic回归的内核从线性输出到概率映射2.1 为什么不能直接用线性回归做分类假设我们要预测一个用户是否会购买某个产品用线性回归去拟合输出值可能是-3到5之间的任意实数。但概率必须落在[0,1]区间内线性回归的输出显然不满足这个约束。有人可能会想那我直接把输出截断到[0,1]不就行了问题在于截断会导致梯度在边界处消失而且线性模型对极端值的敏感度太高——一个异常点就能把整条直线拉偏导致分类边界严重偏移。更本质的问题是线性回归假设误差服从正态分布而二分类问题的目标变量服从伯努利分布。用错误的分布假设去建模得到的参数估计虽然可以计算但统计性质没有保障。所以我们需要一个函数把线性输出映射到(0,1)区间同时保持单调性和可微性。2.2 Sigmoid函数把任意实数压缩成概率Logistic回归选择的映射函数是Sigmoid函数也叫Logistic函数σ(z) 1 / (1 e^(-z))这个函数的形状像一个拉长的S当z趋近正无穷时输出趋近1当z趋近负无穷时输出趋近0当z0时输出恰好是0.5。它的导数有一个非常优雅的性质σ(z) σ(z) * (1 - σ(z))这意味着导数的计算可以直接用函数值本身来表示不需要额外复杂的运算。这个性质在梯度下降推导中非常关键也是Logistic回归训练效率高的原因之一。用生活化的类比来理解Sigmoid函数就像一个证据累积器。你手头有若干条线索特征每条线索的权重不同参数把所有线索加权求和得到一个总分z。如果总分很高说明证据强烈支持是这个结论概率就接近1如果总分很低概率就接近0如果总分在0附近说明证据模棱两可概率就在0.5左右徘徊。2.3 对数几率Logistic回归的线性本质把Sigmoid函数反过来看如果我们已知概率p想求对应的z值可以得到z ln(p / (1-p))这里的p/(1-p)叫做几率odds取对数之后叫对数几率log-odds。Logistic回归的核心假设就是对数几率是特征的线性组合。ln(p / (1-p)) w0 w1*x1 w2*x2 ... wn*xn这个假设的意义在于虽然概率和特征之间是非线性关系但对数几率和特征之间是线性的。这就保留了线性模型的可解释性每个特征的权重wi表示该特征每增加一个单位对数几率的变化量。在医学研究中这个值取指数之后就是优势比odds ratio可以直接解释为风险倍数。2.4 参数估计为什么用最大似然而不是最小二乘线性回归用最小二乘法估计参数因为它的损失函数是均方误差有闭式解。但Logistic回归不能用最小二乘原因有两个一是Sigmoid函数是非线性的代入均方误差后损失函数不再是凸函数可能存在多个局部最优二是二分类问题的目标变量是伯努利分布用最大似然估计才符合统计原理。最大似然估计的思路是找到一组参数使得观测到当前样本的概率最大。对于二分类问题单个样本的似然函数可以写成L(w) ∏ [p_i^y_i * (1-p_i)^(1-y_i)]取对数之后变成l(w) Σ [y_i * ln(p_i) (1-y_i) * ln(1-p_i)]这个对数似然函数是凸函数可以用梯度下降、牛顿法或L-BFGS等优化算法求解。实际工程中大部分机器学习库默认用的是L-BFGS因为它在中小规模数据上收敛速度快且不需要手动调学习率。注意虽然对数似然函数是凸的理论上存在全局最优解但如果数据存在完全分离某个特征能把正负样本完美分开参数会趋向无穷大导致模型无法收敛。这时候需要加正则化项。3. 动手实现从零写一个Logistic回归再到工程化调包3.1 用NumPy手写核心逻辑理解一个模型最好的方式就是自己实现一遍。下面是一个简化版的Logistic回归实现用梯度下降优化import numpy as np class LogisticRegressionFromScratch: def __init__(self, learning_rate0.01, max_iter1000, tol1e-4): self.lr learning_rate self.max_iter max_iter self.tol tol self.weights None self.bias None def _sigmoid(self, z): # 防止溢出 z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def fit(self, X, y): n_samples, n_features X.shape self.weights np.zeros(n_features) self.bias 0.0 for i in range(self.max_iter): linear_output np.dot(X, self.weights) self.bias y_pred self._sigmoid(linear_output) # 计算梯度 dw (1 / n_samples) * np.dot(X.T, (y_pred - y)) db (1 / n_samples) * np.sum(y_pred - y) # 更新参数 self.weights - self.lr * dw self.bias - self.lr * db # 检查收敛 if np.max(np.abs(dw)) self.tol: print(f在第{i}轮迭代收敛) break def predict_proba(self, X): linear_output np.dot(X, self.weights) self.bias return self._sigmoid(linear_output) def predict(self, X, threshold0.5): return (self.predict_proba(X) threshold).astype(int)这段代码虽然简单但包含了Logistic回归训练的全部核心步骤前向计算、梯度计算、参数更新、收敛判断。实际跑一遍之后你会对梯度下降这四个字有完全不同的理解。3.2 手写版和sklearn版的差异对比自己写完再对比sklearn的实现能发现很多工程上的优化细节对比维度手写版sklearn版优化算法批量梯度下降L-BFGS/牛顿法/随机平均梯度正则化无L1/L2/ElasticNet收敛判断梯度最大值多种准则综合多分类支持需手动扩展自动OvR或Softmax数值稳定性简单截断更精细的处理稀疏矩阵支持无完整支持sklearn的LogisticRegression默认使用L2正则化正则化强度由参数C控制。注意这里的C是正则化系数的倒数C越小正则化越强。这个设计和其他库比如SVM保持一致但和有些教材里用λ表示正则化强度的习惯相反初学时容易搞混。3.3 一个完整的实战流程以某虚构的客户流失预测场景为例完整流程如下from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.metrics import classification_report, roc_auc_score import pandas as pd import numpy as np # 假设df是包含特征和标签的数据框 # 特征使用时长、月消费、投诉次数、登录频率等 # 标签是否流失1表示流失 X df.drop(churn, axis1) y df[churn] # 划分训练集和测试集注意stratify保持类别比例 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 标准化Logistic回归对特征尺度敏感 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 网格搜索调参 param_grid { C: [0.01, 0.1, 1, 10, 100], penalty: [l1, l2], solver: [liblinear, saga], class_weight: [None, balanced] } grid GridSearchCV( LogisticRegression(max_iter2000), param_grid, cv5, scoringroc_auc, n_jobs-1 ) grid.fit(X_train_scaled, y_train) # 用最优参数评估 best_model grid.best_estimator_ y_pred best_model.predict(X_test_scaled) y_proba best_model.predict_proba(X_test_scaled)[:, 1] print(classification_report(y_test, y_pred)) print(fAUC: {roc_auc_score(y_test, y_proba):.4f})这段代码里有几个关键决策点值得展开说。为什么要做标准化Logistic回归的梯度下降对特征尺度非常敏感。如果某个特征的范围是0到10000另一个是0到1那么前者的梯度会远大于后者导致参数更新步长极不均衡收敛缓慢甚至震荡。标准化之后所有特征均值为0、方差为1梯度下降的路径更平滑。虽然L-BFGS等二阶方法对尺度不那么敏感但标准化仍然是推荐做法。为什么用stratify如果正负样本比例是1:9随机划分可能导致训练集和测试集的类别比例不一致评估结果波动大。stratify参数保证划分后各类别比例与原始数据一致。class_weightbalanced什么时候用当类别严重不平衡时模型会倾向于预测多数类。设置class_weightbalanced后算法会自动给少数类更高的权重相当于在损失函数里做了代价敏感学习。这是处理不平衡数据最简单有效的方法之一比盲目过采样或欠采样更稳妥。4. 调参和优化的那些门道正则化、求解器与阈值选择4.1 正则化L1和L2到底怎么选Logistic回归的正则化不是为了解决过拟合那么简单它还承担着特征选择和数值稳定的双重角色。L2正则化岭回归风格在损失函数中加入权重平方和会让权重趋向于较小的值但不会变成零。它的优点是处处可导优化稳定适合特征都有用但需要控制权重大小的场景。L1正则化Lasso风格加入权重绝对值之和会产生稀疏解即部分权重被压缩到恰好为零。这意味着模型自动做了特征选择适合高维数据中只有少数特征真正重要的场景。实际选择时我通常的做法是先跑一遍L2看整体效果如果特征维度很高比如超过几百个且怀疑有很多冗余特征再试L1。如果两者效果差不多优先选L2因为L1在特征高度相关时表现不稳定可能随机保留其中一个而丢弃另一个。ElasticNet是L1和L2的混合通过l1_ratio参数控制混合比例。它在特征分组相关时表现更好但多了一个需要调的参数增加了调参成本。4.2 求解器选择不是越高级越好sklearn提供了多种求解器各有适用场景求解器适用正则化适用规模特点liblinearL1/L2小规模坐标下降稳定但慢lbfgsL2/None中大规模拟牛顿法默认选择newton-cgL2/None中大规模牛顿法需要计算HessiansagL2/None大规模稀疏随机平均梯度sagaL1/L2/ElasticNet大规模稀疏sag的改进版支持L1我踩过的一个坑是用默认的lbfgs求解器去跑L1正则化结果直接报错。因为lbfgs不支持L1。后来查文档才知道要用L1必须选liblinear或saga。liblinear适合小数据saga适合大数据。这个细节如果不注意调参时会浪费很多时间。另一个经验是当数据量超过10万条时lbfgs的收敛速度会明显下降这时候换成saga或者sag会快很多但需要更仔细地调学习率和迭代次数。saga在L1正则化下的收敛速度比liblinear快但内存占用更高。4.3 阈值调整0.5不是金科玉律模型输出的是概率默认用0.5作为分类阈值。但在实际业务中0.5往往不是最优选择。比如在欺诈检测场景漏掉一个欺诈交易的代价远大于误判一个正常交易这时候应该降低阈值让更多可疑交易被标记出来。阈值的选择需要结合业务指标。常用的方法有KS曲线找到正负样本累积分布差异最大的点F1分数最大化在精确率和召回率之间找平衡成本敏感分析根据误判成本计算最优阈值PR曲线在不平衡数据中比ROC曲线更敏感我通常会在验证集上画一条阈值-指标曲线然后根据业务需求选定。比如某场景要求召回率不低于90%那就找到满足这个条件的最低阈值再看精确率是否可接受。from sklearn.metrics import precision_recall_curve import matplotlib.pyplot as plt precisions, recalls, thresholds precision_recall_curve(y_test, y_proba) # 找到召回率0.9时的最大精确率 valid_idx recalls[:-1] 0.9 if valid_idx.any(): best_idx np.argmax(precisions[:-1][valid_idx]) best_threshold thresholds[valid_idx][best_idx] print(f推荐阈值: {best_threshold:.4f}) print(f对应精确率: {precisions[valid_idx][best_idx]:.4f}) print(f对应召回率: {recalls[valid_idx][best_idx]:.4f})4.4 特征工程比调参更重要的事说实话在我做过的项目里特征工程带来的提升往往比调参大得多。Logistic回归是线性模型它无法自动捕捉特征之间的交互效应和非线性关系。所以特征工程的核心目标就是把业务知识转化成模型能理解的线性信号。几个实用的技巧分箱化把连续变量离散化成几个区间。比如年龄分成18-25、26-35、36-50、50每个区间用一个哑变量表示。这样做的好处是捕捉非线性关系同时增强鲁棒性。但分箱会损失信息箱数需要权衡。交互特征手动构造特征乘积。比如月消费和在网时长的乘积可能比单独两个特征更有预测力。但交互特征数量会随原始特征数平方增长需要根据业务逻辑筛选。WOE编码在风控领域常用用证据权重Weight of Evidence代替原始类别值。WOE反映了某个类别对目标变量的预测能力编码后的特征与对数几率近似线性非常适合Logistic回归。特征缩放前面提过标准化但对于有偏分布的特征先做对数变换或Box-Cox变换再标准化效果更好。提示做特征工程时一定要在训练集上拟合变换参数然后应用到测试集。如果在全量数据上拟合会造成数据泄露导致评估结果虚高。5. 踩坑实录那些让我重新跑模型的瞬间5.1 完全分离问题当某个特征太完美有一次做一个审批预测模型加入了一个特征叫是否有历史逾期记录。结果发现所有有逾期记录的样本标签都是1没有逾期记录的样本标签都是0。模型训练时loss一直下降但参数越来越大最后报出ConvergenceWarning。这就是典型的完全分离问题。当某个特征能完美区分正负样本时对数似然函数没有有限的最大值参数会趋向无穷大。解决方法有三种一是加L2正则化限制参数大小二是移除这个特征三是使用贝叶斯Logistic回归引入先验分布。我当时的处理是加L2正则化并把C设小一点比如0.1同时检查是否还有其他类似特征。这个问题在金融风控、医疗诊断等领域特别常见因为有些变量本身就是强规则。5.2 多重共线性系数符号反直觉另一个项目里我发现收入这个特征的系数是负的意味着收入越高越容易违约这显然不符合业务常识。排查后发现收入和职业类型高度相关而职业类型里已经包含了收入信息。两个特征同时放入模型导致系数估计不稳定出现了符号反转。多重共线性不会影响预测精度但会严重影响系数解释。如果只是做预测可以不管但如果要解释特征重要性就必须处理。常用的诊断方法是计算方差膨胀因子VIFVIF大于10通常认为存在严重共线性。处理方法包括删除相关特征、使用PCA降维、或者改用带L2正则化的模型岭回归可以缓解共线性。5.3 样本不平衡AUC很高但业务不可用某次做一个罕见事件预测正样本只占2%。模型训练完AUC达到0.95看起来很不错。但上线后发现模型几乎把所有样本都预测为负类因为这样准确率也有98%。这就是不平衡数据下的评估陷阱。解决思路分两个层面。数据层面可以用过采样SMOTE、欠采样或混合采样。算法层面可以设置class_weight、调整阈值、或者改用Focal Loss等代价敏感损失函数。评估指标也要从准确率换成AUC、F1、KS等对不平衡不敏感的指标。我个人的经验是先试class_weight简单有效如果效果不够再试SMOTE但要注意SMOTE会在少数类样本之间插值可能生成不存在的样本在高维空间中尤其危险。最好结合欠采样使用比如SMOTETomek Links。5.4 数据泄露标准化顺序搞反了这个坑我踩过不止一次。正确的流程是先划分训练集和测试集然后在训练集上fit标准化器再用这个标准化器transform测试集。如果先对全量数据做标准化再划分测试集的统计信息就泄露到了训练过程中导致评估结果偏乐观。同样的道理适用于特征选择、PCA降维、缺失值填充等所有涉及数据统计的步骤。我现在的习惯是把所有预处理步骤封装进Pipeline这样从流程上杜绝泄露。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(C1.0, max_iter2000)) ]) # 交叉验证时Pipeline会自动在每折的训练数据上fit预处理器 from sklearn.model_selection import cross_val_score scores cross_val_score(pipe, X, y, cv5, scoringroc_auc) print(f交叉验证AUC: {scores.mean():.4f} ± {scores.std():.4f})6. 模型评估与业务落地从AUC到实际决策6.1 分类报告怎么看才不流于形式classification_report输出的精确率、召回率、F1分数很多人只是看一眼就过了。但每个指标背后对应着不同的业务含义。精确率Precision回答的是模型说是正类的样本里有多少真的是正类。在垃圾邮件过滤中精确率低意味着很多正常邮件被误判为垃圾邮件用户体验差。召回率Recall回答的是真正的正类样本里模型找出了多少。在疾病筛查中召回率低意味着很多患者被漏诊后果严重。F1是两者的调和平均适合需要平衡的场景。但在实际业务中精确率和召回率的权重往往不对等这时候应该用F-beta分数或者直接根据业务成本计算期望损失。6.2 ROC曲线和PR曲线的选择ROC曲线以假正率FPR为横轴真正率TPR为纵轴。它对类别比例不敏感适合评估模型整体的排序能力。但在极度不平衡的数据中ROC曲线可能过于乐观因为FPR的分母是负类样本数负类很多时FPR变化不明显。PR曲线以召回率为横轴精确率为纵轴。它直接反映正类的识别质量在不平衡数据中更有参考价值。如果正类是少数类且你关心的是正类的识别效果优先看PR曲线。我通常两个都看ROC看整体排序PR看正类识别。如果两者结论矛盾以PR为准当正类稀少时。6.3 概率校准模型说70%就真的是70%吗Logistic回归输出的概率理论上应该是校准的即预测概率为0.7的样本中实际正类比例应该接近70%。但在实际数据中由于正则化、特征选择、样本偏差等原因概率可能偏离真实值。如果业务需要精确的概率估计比如定价、风险评估就需要做概率校准。常用的方法有Platt Scaling和Isotonic Regression。sklearn提供了CalibratedClassifierCV可以方便地对任何分类器做校准。from sklearn.calibration import CalibratedClassifierCV calibrated CalibratedClassifierCV( LogisticRegression(C1.0), methodsigmoid, # Platt Scaling cv5 ) calibrated.fit(X_train_scaled, y_train)校准之后可以用可靠性图Reliability Diagram来验证效果。如果校准曲线接近对角线说明概率估计可靠。6.4 模型部署的轻量化优势Logistic回归在部署阶段有一个巨大优势模型就是一个线性方程加一个Sigmoid函数。这意味着你可以把训练好的权重导出成JSON或CSV然后在任何语言、任何平台上实现推理不需要深度学习框架不需要GPU甚至不需要网络请求。我做过一个边缘设备上的实时预测项目模型就是Logistic回归。把权重导出后用C语言写了一个几十行的推理函数直接嵌入到设备固件里延迟在微秒级别。这种轻量化是复杂模型无法比拟的。导出权重的示例import json model_info { weights: best_model.coef_[0].tolist(), intercept: best_model.intercept_[0], feature_names: X.columns.tolist(), threshold: 0.5 } with open(logistic_model.json, w) as f: json.dump(model_info, f, indent2)推理时只需要读取权重做点积运算再过Sigmoid函数即可。这种透明性也让模型审计变得容易——监管方可以直接检查每个特征的权重是否符合业务逻辑。7. 多分类扩展与进阶方向7.1 从二分类到多分类OvR和SoftmaxLogistic回归原生是二分类器但可以通过两种策略扩展到多分类。One-vs-RestOvR为每个类别训练一个二分类器将该类别作为正类其余作为负类。预测时选择概率最高的类别。优点是简单每个分类器可以独立训练缺点是当类别很多时每个分类器的正负样本极度不平衡且各类别概率之和不等于1。Softmax回归直接建模多分类概率分布用Softmax函数替代SigmoidP(yk|x) exp(z_k) / Σ exp(z_j)Softmax回归的参数估计用最大似然损失函数是交叉熵。它保证了所有类别概率之和为1且类别之间相互竞争。当类别互斥时Softmax通常优于OvR。sklearn的LogisticRegression默认对多分类问题使用OvR除了saga求解器支持multinomial。如果要用Softmax需要设置multi_classmultinomial和solverlbfgs或saga。7.2 贝叶斯Logistic回归引入先验解决小样本问题当数据量很少或者存在完全分离时频率派的Logistic回归会遇到困难。贝叶斯Logistic回归通过引入参数的先验分布通常是高斯分布得到后验分布而不是点估计。这样做的好处是即使数据完全分离后验分布仍然有界同时可以提供参数的不确定性估计。实现上可以用PyMC或Stan做MCMC采样也可以用变分推断加速。虽然计算成本比sklearn高但在小样本、高维、需要不确定性量化的场景下很有价值。7.3 在线学习用SGD处理流式数据如果数据是持续到来的流式数据无法一次性加载到内存可以用随机梯度下降SGD做在线学习。sklearn的SGDClassifier支持partial_fit方法可以逐批更新模型。from sklearn.linear_model import SGDClassifier online_model SGDClassifier( losslog_loss, # 等价于Logistic回归 learning_rateoptimal, penaltyl2, alpha0.0001 ) # 模拟流式数据 for batch_X, batch_y in data_stream: online_model.partial_fit(batch_X, batch_y, classes[0, 1])在线学习的挑战在于学习率调度和概念漂移处理。如果数据分布随时间变化模型需要遗忘旧模式、适应新模式。常用的技巧包括自适应学习率、滑动窗口训练、以及定期用全量数据重新训练。7.4 与其他模型的对比什么时候不该用Logistic回归Logistic回归不是万能的。以下场景建议考虑其他模型特征与对数几率明显非线性尝试核方法SVM、梯度提升树或神经网络特征交互复杂且未知树模型自动捕捉交互省去手动构造数据量极大且特征维度极高线性SVM或朴素贝叶斯可能更快需要处理序列或图像数据Logistic回归无能为力需要RNN或CNN但反过来如果需要一个快速、可解释、稳定、易部署的基线模型Logistic回归几乎总是第一个应该尝试的。我个人的习惯是任何二分类项目先跑一个Logistic回归作为baseline然后再尝试复杂模型。如果复杂模型没有显著提升就果断用Logistic回归省时省力还省心。8. 我在实际项目中的几点体会做了这么多项目关于Logistic回归有几个体会是反复被验证的。第一不要跳过探索性数据分析。很多人拿到数据直接调包结果模型效果不好就怪算法。其实大部分问题出在数据本身缺失值处理不当、异常值未处理、特征分布偏斜、类别标签错误。花时间做EDA比调参的回报率高得多。第二正则化不是可选项而是必选项。即使数据量充足加一点L2正则化也能提升泛化能力。我通常从C1.0开始然后根据交叉验证结果调整。如果发现训练集和验证集差距大就减小C如果两者都差说明欠拟合增大C或增加特征。第三概率输出比类别输出更有价值。尽量把predict_proba的结果保留下来让业务方根据实际需求决定阈值。直接给一个0/1的判断等于替业务方做了决策往往不是最优的。第四模型文档和版本管理很重要。Logistic回归的系数可以直接解释但前提是你知道每个特征的含义、量纲、以及训练时的预处理步骤。我习惯在模型文件里附带特征说明、训练日期、数据版本、评估指标方便后续追溯和审计。第五简单模型的上限往往被低估。在结构化数据上经过精心特征工程的Logistic回归效果经常能逼近甚至超过复杂模型。而且它的训练时间以秒计推理时间以微秒计维护成本极低。在追求效果的同时别忘了工程上的性价比。最后分享一个实用技巧如果特征数量在几十到几百之间可以用递归特征消除RFE配合Logistic回归做特征选择。RFE每次训练模型、移除最不重要的特征、再训练直到达到指定特征数。虽然计算量比单次训练大但能显著提升模型简洁性和泛化能力。配合交叉验证使用效果更稳。