模型泛化
模型泛化
泛化即是,机器学习模型学习到的概念在它处于学习的过程中时模型没有遇见过的样本时候的表现。
类似于考试前疯狂刷题不管这些题做的都好都没用,只有在下次考试中才有效果,而下次考试的那些题可能从来没见过,只有学会这些基础的东西,无论怎么变都能认出来。模型也是一样,不管训练集上表现如何,只有在新的未知的样本集上有较好的表现,模型才是真的好,这就是模型的泛化能力。
一、交叉验证
- 首先讨论一下测试数据集的意义。假如将所有的训练数据进行训练处一个模型,此时如果模型发生了过拟合却不自知,在训练集上的表现误差很小,但是很有可能模型的泛化能力不足,产生了过拟合。所以相应的我们就要把数据分为训练数据和测试数据,通过测试数据集来判断模型的好坏。
- 针对特定的测试数据集过拟合。
通常情况下如果将数据分为训练数据和测试数据,通过测试数据对模型的验证从而调整训练模型,也就说我们在围绕着测试集进行打转,设法在训练数据中找到一组参数,在测试数据上表现最好,既然是这样,就很有可能针对特定的测试数据集产生了过拟合。由此就引出了验证集。
- 验证集。
测试数据集作为衡量最终模型性能的数据集。测试数据不参与模型创建,而训练数据参与模型的训练,验证数据集参与模型评判,一旦效果不好就进行相应的调整重新训练,这两者都参与了模型的创建。验证数据集用来调整超参数。其实,这么做还是存在一定的问题,那就是这个验证数据集的随机性,因为很有可能对这一份验证集产生了过拟合。因此就有了交叉验证(Cross Validation)。
我们将训练数据随机分为k份,上图中分为k=3份,将任意两种组合作为训练集,剩下的一组作为验证集,这样就得到k个模型,然后在将k个模型的均值作为结果调参。显然这种方式要比随机只用一份数据作为验证要靠谱的多。
接下里就用实际的例子,来了解一下如何使用交叉验证进行调参:
- 第一种情况:只使用训练集测试集测试:
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
digits = datasets.load_digits()
x = digits.data
y = digits.target
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.4, random_state=666)
best_score, best_p, best_k = 0, 0, 0
for k in range(2, 11):
for p in range(1, 6):
knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=k, p=p)
knn_clf.fit(x_train, y_train)
score = knn_clf.score(x_test, y_test)
if score > best_score:
best_score, best_p, best_k = score, p, k
print("Best K=", best_k)
print("Best P=", best_p)
print("Best score=", best_score)
输出结果:
Best K= 3
Best P= 4
Best score= 0.9860917941585535
- 第二种情况:
from sklearn.model_selection import cross_val_score
knn_clf = KNeighborsClassifier()
cross_val_score(knn_clf, x_train, y_train)
# array([0.98895028, 0.97777778, 0.96629213])
根据输出结果,可以看出在sklearn中封装的交叉验证默认情况下分成三份。
best_score, best_p, best_k = 0, 0, 0
for k in range(2, 11):
for p in range(1, 6):
knn_clf = KNeighborsClassifier(weights="distance", n_neighbors=k, p=p)
scores = cross_val_score(knn_clf, x_train, y_train)
score = np.mean(scores)
if score > best_score:
best_score, best_p, best_k = score, p, k
print("Best K=", best_k)
print("Best P=", best_p)
print("Best score=", best_score)
Best K= 2
Best P= 2
Best score= 0.9823599874006478
对比第一种情况,我们发现得到的最优超参数是不一样的,虽然score会稍微低一些,但是一般第二种情况更加可信。但是这个score只是说明这组参数最优,并不是指的是模型对于测试集的准确率,因此接下来看一下准确率。
best_knn_clf = KNeighborsClassifier(weights='distance', n_neighbors=2, p=2)
best_knn_clf.fit(x_train, y_train)
best_knn_clf.score(x_test, y_test)
输出结果:0.980528511821975,这才是模型的准确度。
如果想要修改默认的三份,再传入一个参数就可以了。同样地,下面的网格搜索中的cv也是可以调整的。
cross_val_score(knn_clf, x_train, y_train, cv=5)
输出结果:array([0.99543379, 0.97716895, 0.97685185, 0.98130841, 0.97142857])
- 网格搜索中的交叉验证。
其实回顾网格搜索的过程就使用了交叉验证的手段。
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
param_grid = [
{
'weights':['distance'],
'n_neighbors':[i for i in range(2, 11)],
'p':[i for i in range(1, 6)]
}
]
knn_clf = KNeighborsClassifier()
grid_search = GridSearchCV(knn_clf, param_grid, verbose=1)
grid_search.fit(x_train, y_train)
Fitting 3 folds for each of 45 candidates, totalling 135 fits
[Parallel(n_jobs=1)]: Done 135 out of 135 | elapsed: 1.3min finished
GridSearchCV(cv='warn', error_score='raise-deprecating',
estimator=KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=None, n_neighbors=5, p=2,
weights='uniform'),
fit_params=None, iid='warn', n_jobs=None,
param_grid=[{'weights': ['distance'], 'n_neighbors': [2, 3, 4, 5, 6, 7, 8, 9, 10], 'p': [1, 2, 3, 4, 5]}],
pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
scoring=None, verbose=1)
grid_search.best_score_
# 0.9823747680890538
grid_search.best_params_
# {'n_neighbors': 2, 'p': 2, 'weights': 'distance'}
best_knn_clf = grid_search.best_estimator_
best_knn_clf.score(x_test, y_test)
# 0.980528511821975
这跟上面使用交叉验证的到结果一致。
-
k-折交叉验证
把训练集分成k份,称为k-folds cross validation,缺点就是,每次训练k个模型,相当于整体性能慢了k倍。不过通常这种方法是最值得信赖的。
-
留一法LOO-CV
在极端情况下。假设数据集中有m个样本,我们就把数据集分为m份,称为留一法。(Leave-One-Out Cross Validation),这样做的话,完全不受随机的影响,最接近模型真正的性能指标,缺点就是计算量巨大。
二、偏差方差权衡
偏差方差权衡(Bias Variance Trade off),当我们的模型表现不佳时,通常是出现两种问题,一种是 高偏差 问题,另一种是 高方差 问题。识别它们有助于选择正确的优化方式,所以我们先来看下 偏差 与 方差 的意义。
- 偏差: 描述模型输出结果的期望与样本真实结果的差距。
- 方差: 描述模型对于给定值的输出稳定性。 方差越大模型的泛华能力越弱。
就像打靶一样,偏差描述了我们的射击总体是否偏离了我们的目标,而方差描述了射击准不准。左一是方差跟偏差都很小,都比较靠近中心且集中,右一分散在中心附近,但比较散,因此方差较大。这样结合下面这两幅图就可以大概理解,偏差描述的是描述模型输出结果的期望与样本真实结果的差距。而方差则是对于输出结果是否集中,描述模型对于给定值的输出稳定性。
模型误差 = 偏差 + 方差 + 不可避免的误差
- 导致偏差大的原因:对问题本身的假设不正确!如非线性数据使用线性回归。或者特征对应标记高度不相关也会导致高偏差,不过这是对应着特征选择,跟算法没有关系,对于算法而言基本属于欠拟合问题underfitting。
- 导致方差大的原因:数据的一点点扰动都会极大地影响模型。通常原因就是使用的模型太复杂,如高阶多项式回归。这就是所说的过拟合(overfitting)
- 总结:有些算法天生就是高方差的算法,如KNN,非参数学习的算法通常都是高方差的,因为不对数据进行任何假设。还有一些算法天生就是高偏差的,如线性回归。参数学习通常都是高偏差算法,因为对数据具有极强的假设。大多数算法具有相应的算法可以调整偏差和方差,如KNN中的k,如线性回归中使用多项式回归中的degree,偏差和方差通常是矛盾的,降低偏差,会提高方差,降低方差,会提高偏差,因此在实际应用中需要进行权衡。机器学习的主要挑战,在于方差。这句话只针对算法,并不针对实际问题。因为大多数机器学习需要解决过拟合问题。
- 解决手段:
- 降低模型复杂度
- 减少数据维度;降噪
- 增加样本数量
- 使用验证集
- 模型正则化
三、模型正则化
模型正则化(Regularization):限制参数的大小。常常用来解决过拟合问题。接下来具体看一下:
先看一下多项式回归过拟合的情况:
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
import numpy as np
import matplotlib.pyplot as plt
def PolynomiaRegression(degree):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('lin_reg', LinearRegression()),
])
np.random.seed(666)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x**2 + x + 2 + np.random.normal(0, 1, size=100)
poly100_reg = PolynomiaRegression(degree=100)
poly100_reg.fit(X, y)
y100_predict = poly100_reg.predict(X)
mean_squared_error(y, y100_predict)
# 0.687293250556113
x_plot = np.linspace(-3, 3, 100).reshape(100, 1)
y_plot = poly100_reg.predict(x_plot)
plt.scatter(x, y)
plt.plot(x_plot[:,0], y_plot, color='r')
plt.axis([-3, 3, 0, 10])
plt.show()
lin_reg.coef_
array([ 1.21093453e+12, 1.19203091e+01, 1.78867645e+02, -2.95982349e+02,
-1.79531458e+04, -1.54155027e+04, 8.34383276e+05, 8.19774042e+05,
-2.23627851e+07, -1.44771550e+07, 3.87211418e+08, 1.13421075e+08,
-4.61600312e+09, -1.25081501e+08, 3.93150405e+10, -5.47576783e+09,
-2.44176251e+11, 5.46288687e+10, 1.11421043e+12, -2.76406464e+11,
-3.71329259e+12, 8.55454910e+11, 8.80960804e+12, -1.60748867e+12,
-1.39204160e+13, 1.49444708e+12, 1.19236879e+13, 2.47473079e+11,
4.42409192e+11, -1.64280931e+12, -1.05153597e+13, -1.80898849e+11,
3.00205050e+12, 2.75573418e+12, 8.74124346e+12, -1.36695399e+12,
-1.22671920e+12, -7.00432918e+11, -8.24895441e+12, -8.66296096e+11,
-2.75689092e+12, 1.39625207e+12, 6.26145077e+12, -3.47996080e+11,
6.29123725e+12, 1.33768276e+12, -6.11902468e+11, 2.92339251e+11,
-6.59758587e+12, -1.85663192e+12, -4.13408727e+12, -9.72012430e+11,
-3.99030817e+11, -7.53702123e+11, 5.49214630e+12, 2.18518119e+12,
5.24341931e+12, 7.50251523e+11, 5.50252585e+11, 1.70649474e+12,
-2.26789998e+12, -1.84570078e+11, -5.47302714e+12, -2.86219945e+12,
-3.88076411e+12, -1.19593780e+12, 1.16315909e+12, -1.41082803e+12,
3.56349186e+12, 7.12308678e+11, 4.76397106e+12, 2.60002465e+12,
1.84222952e+12, 3.06319895e+12, -1.33316498e+12, 6.18544545e+11,
-2.64567691e+12, -1.01424838e+12, -4.76743525e+12, -3.59230293e+12,
-1.68055178e+12, -3.57480827e+12, 2.06629318e+12, -6.07564696e+11,
3.40446395e+12, 3.42181387e+12, 3.31399498e+12, 4.92290870e+12,
3.79985951e+11, 1.34189037e+12, -3.34878352e+12, -2.07865615e+12,
-3.24634078e+12, -5.48903768e+12, 5.87242630e+11, -2.27318874e+12,
2.60023097e+12, 8.21820883e+12, 4.79532121e+10, -3.11436610e+12,
-6.27736909e+11])
通过查看多项式回归的系数可以发现,有些系数能差13个数量级,其实这就是过拟合了!而模型正则化就是为了解决这个问题。先来回顾一下多项式回归的目标。
-
加入正则项之前
目标:使\sum_{i=1}m(y - \theta_{0} - \theta_{1}X_{1}^{(i)} - \theta_{2}X_{2}^{(i)} - \dots - \theta_{n}X_{n}{(i)})2尽可能的小,
目标:使J(\theta) = MSE(y, \haty, \theta)尽可能地小
-
加入正则项之后
目标:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \theta_{i}^2尽可能小
通过加入的正则项来控制系数不要太大,从而使曲线不要那么陡峭,变化的那么剧烈。在这里有几个细节需要注意。
- 第一点:\theta从1开始,只包含系数不包括截距,这是因为截距只决定曲线的高低,并不会影响曲线的陡峭和缓和。
- 第2点:就是这个\frac{1}{2},是为了求导之后能够将2消去,为了方便计算。不过其实这个有没有都是可以的,因为在正则化前有一个系数,我们可以把这个\frac{1}{2}可以考虑到\alpha中去。
- 第3点:系数\alpha,它表示正则化项在整个损失函数中所占的比例。极端一下,\alpha=0时,相当于模型没有加入正则化,但如果\alpha = 正无穷,此时其实主要的优化任务就变成了需要所有的\theta都尽可能的小,最优的情况就是全为0。至于\alpha的取值就需要尝试了。
1、岭回归(Ridege Regression)
目标:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \theta_{i}^2尽可能小
测试用例:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
x = np.random.uniform(-3.0, 3.0, size=100)
X = x.reshape(-1, 1)
y = 0.5 * x + 3 + np.random.normal(0, 1, size=100)
plt.scatter(x, y)
plt.show()
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
def PolynomiaRegression(degree):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('lin_reg', LinearRegression()),
])
np.random.seed(666)
x_train, x_test, y_train, y_test = train_test_split(X, y)
poly_reg = PolynomiaRegression(degree=20)
poly_reg.fit(x_train, y_train)
y_poly_predict = poly_reg.predict(x_test)
mean_squared_error(y_test, y_poly_predict)
# 167.9401085999025
import matplotlib.pyplot as plt
x_plot = np.linspace(-3, 3, 100).reshape(100, 1)
y_plot = poly_reg.predict(x_plot)
plt.scatter(x, y)
plt.plot(x_plot[:,0], y_plot, color='r')
plt.axis([-3, 3, 0, 6])
plt.show()
把画图这些操作封装成一个函数,方便后面调用:
def plot_model(model):
x_plot = np.linspace(-3, 3, 100).reshape(100, 1)
y_plot = model.predict(x_plot)
plt.scatter(x, y)
plt.plot(x_plot[:,0], y_plot, color='r')
plt.axis([-3, 3, 0, 6])
plt.show()
使用岭回归:
from sklearn.linear_model import Ridge
def RidgeRegression(degree, alpha):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('lin_reg', Ridge(alpha=alpha)),
])
ridege1_reg = RidgeRegression(20, alpha=0.0001)
ridege1_reg.fit(x_train, y_train)
y1_predict = ridege1_reg.predict(x_test)
mean_squared_error(y_test, y1_predict)
# 1.3233492754136291
# 跟之前的136.相比小了很多
plot_model(ridege1_reg)
调整\alpha
ridege2_reg = RidgeRegression(20, alpha=1)
ridege2_reg.fit(x_train, y_train)
y2_predict = ridege2_reg.predict(x_test)
mean_squared_error(y_test, y2_predict)
# 1.1888759304218461
plot_model(ridege2_reg)
ridege3_reg = RidgeRegression(20, alpha=100)
ridege3_reg.fit(x_train, y_train)
y3_predict = ridege3_reg.predict(x_test)
mean_squared_error(y_test, y3_predict)
# 1.3196456113086197
# 此时相比alpha=1时均方误差上升了,说明可能正则过头了
plot_model(ridege3_reg)
ridege4_reg = RidgeRegression(20, alpha=1000000)
ridege4_reg.fit(x_train, y_train)
y4_predict = ridege4_reg.predict(x_test)
mean_squared_error(y_test, y4_predict)
# 1.8404103153255003
plot_model(ridege4_reg)
这也跟之前分析,如果alpha=正无穷时,为了使损失函数最小,就需要所有的系数的平方和最小,即\theta都趋于0。通过上面几种alpha的取值可以看出我们可以在1-100进行更加细致的搜索,找到最合适的一条相对比较平滑的曲线去拟合。这就是L2正则。
2、LASSO Regularization
目标:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \mid \theta_{i}尽可能小
LASSO: Least Absolute Shrinkage and Selection Operator Regression
Shrinkage:收缩,缩小,收缩量。特征缩减。重点在于Selection Operator
使用lasso回归:
from sklearn.linear_model import Lasso
def LassoRegression(degree, alpha):
return Pipeline([
('poly', PolynomialFeatures(degree=degree)),
('std_scale', StandardScaler()),
('lin_reg', Lasso(alpha=alpha)),
])
lasso1_reg = LassoRegression(20, 0.01)
#这里相比Ridge的alpha小了很多,这是因为在Ridge中是平方项
lasso1_reg.fit(x_train, y_train)
y1_predict = lasso1_reg.predict(x_test)
mean_squared_error(y_test, y1_predict)
# 1.149608084325997
plot_model(lasso1_reg)
lasso2_reg = LassoRegression(20, 0.1)
lasso2_reg.fit(x_train, y_train)
y2_predict = lasso2_reg.predict(x_test)
mean_squared_error(y_test, y2_predict)
# 1.1213911351818648
plot_model(lasso2_reg)
lasso3_reg = LassoRegression(20, 1)
lasso3_reg.fit(x_train, y_train)
y3_predict = lasso3_reg.predict(x_test)
mean_squared_error(y_test, y3_predict)
# 1.8408939659515595
plot_model(lasso3_reg)
3、解释Ridge和LASSO
-
Ridge:目标:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \theta_{i}^2尽可能小
-
Lasso:目标:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \mid \theta_{i}尽可能小
通过这两幅图进行对比发现,LASSO拟合的模型更倾向于是一条直线,而Ridge拟合的模型更趋向与一条曲线。这是因为两个正则的本质不同,Ridge是趋向于使所有\theta的加和尽可能的小,而Lasso则是趋向于使得一部分\theta的值变为0,因此可作为特征选择用,这也是为什么叫Selection Operation的原因。
下面就对上面这两句话尝试着进行一下解释:
-
Ridge Regression:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \theta_{i}^2
当alpha趋近于无穷时,J(\theta) = \alpha \frac{1}{2} \sum_{i=1}^n \theta_{i}^2
\bigtriangledown = \alpha \begin {bmatrix}
\theta_{1} \\
\theta_{2} \\
\dots \\
\theta_{n} \\
\end {matrix}
假设从上图一点开始,导数\bigtriangle中的\theta都是有值的,顺着梯度方向下降。Ridge是趋向于使所有\theta的加和尽可能的小,而不是像lasso一样直接为0。
-
LASSO Regression:J(\theta) = MSE(y, \haty, \theta) + \alpha \frac{1}{2} \sum_{i=1}^n \mid \theta_
当alpha趋近于无穷时,J(\theta) = \alpha \frac{1}{2} \sum_{i=1}^n \mid \theta_{i}
它其实是不可导的,不过也可以简单地使用一个分类函数作为它的导数,
\bigtrianlgedown = \alpha \begin {bmatrix}
sign(\theta_{1}) \\
sign(\theta_{2}) \\
\dots \\
sign(\theta_{n}) \\
\end
其中sign(x) = \begin {cases}
1, \quad x>0 \\
0,\quad x=0 \\
-1, \quad x<0 \\
\end {cases}
所以,当如果从上图的一点开始进行梯度下降的话,就不能想Ridge一样曲线地去逼近0,而是只能使用这些非常规则的方式去逼近零点。在这种路径的梯度下降中,就会达到某些轴的零点,Lasso则是趋向于使得一部分\theta的值变为0。所以可以作为特征选择用。不过也正是因为这样的特性,使得Lasso这种方法有可能会错误将原来有用的特征的系数变为0,所以相对Ridge来说,准确率还是Ridge相对较好一些,但是当特征特别大时候,此时使用Lasso也能将模型的特征变少的作用。
4、比较Ridge和Lasso
-
Rigde:\frac{1}{2}\sum_{i=1}^n \theta_{i}^2 \qquad \qquad Lasso: \sum_{i=1}^n \mid \theta_
-
MSE: \frac{1}{n}\sum_{i=1}^n(y_{i} - \hat y_{i})^2 \qquad MAE: \frac{1}{n} \sum_{i=1}^n \mid (y_{i} - \hat y_{i})
-
欧拉距离:\sqrt{\sum_{i=1}^n (x_{i}^{(1)} - x_{i}^{(2)} ^2} 曼哈顿距离:\sum_{i=1}^n \mid {(x_{i}^{(1)} - x_{i}^
这三者背后的数学思想是非常相近的,只是用在不同的背景下具有不同的效果。对比这三者之后,我们在进行一下拓展:
明可夫斯基距离:(Minkowski Distance) (\sum_{i=1}^n \mid {X_{i}^{(a)} - X_{i}{(b)}}p)^{\frac{1}{p}}
L1正则、L2正则:
\parallel {X_{p}} = (\sum_{i=1}^n \mid{X_{i}}p){p}}这就是传说中的p范数。
所以当p=1时,就是L1范数,对应着Lasso的L1正则项、MAE、曼哈顿距离
当p=2时就是L2范数,对应着Ridge的L2的正则项、MSE、欧拉距离
其实对应着还有L0正则:J(\theta) = MSE(y, \hat{y}; \theta) + min{number-of-non-zero-\theta}
就是让\theta个数越少越好,进而对损失函数进行修正,不过很少使用,这是因为L0正则的优化是一个NP难问题,不能使用梯度下降法或者直接求出一个数学公式来直接找出最优解,这是一个离散最优化的问题,需要穷举所有的\theta为0的组合,从而选出最优的。
范数:具有长度概念的函数,其为向量空间内的所有向量赋予非零的正常度或大小。它是实数的绝对值、复数的模、三维几何空间R[^3]中向量长度的推广。
范数具有三个性质:非负性、齐次性、三角不等式
NP问题:NP完全问题(NP-C问题),是世界七大数学难题之一。 NP的英文全称是Non-deterministic Polynomial的问题,即多项式复杂程度的非确定性问题。简单的写法是 NP=P?,问题就在这个问号上,到底是NP等于P,还是NP不等于P。
5、弹性网
J(\theta) = MSE(y, \hat{y}; \theta) + r \alpha \sum_{i=1}^n \mid{\theta_{i}} + \frac{1-r}{2} \alpha \sum_{i=1}^n ({\theta_{i})^2
这是一个将岭回归和Lasso回归结合的一种变形。结合两者的优势,这就是弹性网(Elastic Net)。
其实这种思想跟之前提到的批量梯度下降法和随机梯度下降法的结合的小批量梯度下降法(mini-batch Gradient Descent)。
我是尾巴
本节学习的主要内容就是如何提高模型的泛化能力,就是我们不仅关注模型在训练集上的表现,还更加要关注模型对于未知的新的数据的预测的准确性,当然方法远不止这些,后续继续学习。
本次推荐:
毒鸡汤:哪有什么选择恐惧症,还不是因为穷;哪有什么优柔寡断,还不是因为怂。
继续加油!!!