线性回归和线性分类器
最小二乘法
对线性模型而言,依赖函数的形式如下:
如果为每项观测加上一个虚维度 x0=1(比如偏置),那么就可以把 w0整合进求和项中,改写为一个略微紧凑的形式:
如果有一个特征观测矩阵,其中矩阵的行是数据集中的观测,那么需要在左边加上一列。由此,线性模型可以定义为:
其中:
上述表达式亦可这样写:
模型具有如下限制(否则它就不是线性回归了):
再引入一项定义:当期望值等于估计参数的真实值时,权重估计被称为无偏(unbiased):
为了解决这一优化问题,需要计算模型参数的导数。将导数设为零,然后求解关于 \textbf ww 的等式,倘若不熟悉矩阵求导,可以参考下面的 4 个式子:
根据高斯-马尔可夫定理,模型参数的 OLS 估计是所有线性无偏估计中最优的,即通过 OLS 估计可以获得最低的方差。
为何选择最小化均方误差而不是其他指标?因为若不选择最小化均方误差,那么就不满足高斯-马尔可夫定理的条件,得到的估计将不再是最佳的线性无偏估计。
最大似然估计
首先举一个简单的例子,我们想做一个试验判定人们是否记得简单的甲醇化学式 CH3OH。首先调查了 400 人,发现只有 117 个人记得甲醇的化学式。那么,直接将117/400≈0.29 作为估计下一个受访者知道甲醇化学式的概率是较为合理的。这个直观的估计就是一个最大似然估计。伯努利分布的定义:如果一个随机变量只有两个值(1 和 0,相应的概率为 θ 和1−θ),那么该随机变量满足伯努利分布
这一分布正是我们所需要的,分布参数 θ 就是「某个人知道甲醇化学式」的概率估计。在 400 个独立试验中,试验的结果记为 x=(x1,x2,…,x400)。写下数据的似然,即观测的可能性,比如正好观测到 117 个随机变量x=1 和 283 个随机变量x=0 的可能性:
接着,将最大化这一 \thetaθ 的表达式。一般而言,为了简化计算,并不最大化似然 p(x;θ),转而最大化其对数(这种变换不影响最终答案):
为了找到最大化上式的θ 值,将上式对θ 求导,并令其为零,求解所得等式:
由上可知,我们的直观估计正好是最大似然估计。现在将这一推理过程应用到线性回归问题上,尝试找出均方误差背后的道理。为此,需要从概率论的角度来看线性回归。我们的模型和之前是一样的:
不过,现在假定随机误差符合均值为零的 正态分布:
由于样本是独立抽取的(误差不相关是高斯-马尔可夫定理的条件之一),数据的似然看起来会是密度函数 p(yi) 的积。
所以,当测量误差服从正态(高斯)分布的情况下, 最小二乘法等价于极大似然估计。
偏置-方差分解
下面讨论线性回归预测的误差性质(可以推广到机器学习算法上),上文提到:
任何线性模型的预测误差由三部分组成:
尽管无法消除σ2,但我们可以影响前两项。理想情况下,希望同时消除偏差和方差(见下图中左上),但是在实践中,常常需要在偏置和不稳定(高方差)间寻找平衡。
一般而言,当模型的计算量增加时(例如,自由参数的数量增加了),估计的方差(分散程度)也会增加,但偏置会下降,这可能会导致过拟合现象。另一方面,如果模型的计算量太少(例如,自由参数过低),这可能会导致欠拟合现象。
线性回归的正则化
这样的解降低了方差,但增加了偏置,因为参数的正则向量也被最小化了,这导致解朝零移动。在下图中,OLS 解为白色虚线的交点,蓝点表示岭回归的不同解。可以看到,通过增加正则化参数 \lambdaλ,使解朝零移动。
线性分类
线性分类器背后的基本思路是,目标分类的值可以被特征空间中的一个超平面分开。如果这可以无误差地达成,那么训练集被称为线性可分。
上面已经介绍了线性回归和普通最小二乘法(OLS)。现在考虑一个二元分类问题,将目标分类记为「+1」(正面样本)和「-1」(负面样本)。最简单的线性分类器可以通过回归定义:
其中:
基于逻辑回归的线性分类器
逻辑回归是线性分类器的一个特殊情形
逻辑回归不仅能够预测样本是「+1」还是「-1」,还能预测其分别是「+1」和「-1」的概率是多少。对于很多业务问题(比如,信用评分问题)而言,这是一个非常重要的优点。下面是一个预测贷款违约概率的例子。
银行选择一个阈值p∗以预测贷款违约的概率(上图中阈值为0.15),超过阈值就不批准贷款。
为了预测概率p+∈[0,1],使用 OLS 构造线性预测:
为了将所得结果转换为 [0,1] 区间内的概率,逻辑回归使用下列函数进行转换:
使用 Matplotlib 库画出上面这个函数。
import warnings
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
warnings.filterwarnings('ignore')
def sigma(z):
return 1. / (1 + np.exp(-z))
xx = np.linspace(-10, 10, 1000)
plt.plot(xx, [sigma(x) for x in xx])
plt.xlabel('z')
plt.ylabel('sigmoid(z)')
plt.title('Sigmoid function')
让我们看看逻辑回归是如何做出预测的:
现在,假设已经通过某种方式得到了权重w,即模型已经训练好了,逻辑回归预测的步骤如下:
所以,逻辑回归预测一个样本分配为「+」分类的概率(假定已知模型的特征和权重),这一预测过程是通过对权重向量和特征向量的线性组合进行 sigmoid 变换完成的,公式如下:
最大似然估计和逻辑回归
现在,看下从最大似然估计(MLE)出发如何进行逻辑回归优化,也就是最小化逻辑损失函数。前面已经见过了将样本分配为「+」分类的逻辑回归模型:
现在,计算数据集的似然,即基于数据集 \textbf{x}x 观测到给定向量 \textbf{y}y 的概率。假设目标来自一个独立分布,然后可建立如下公式:
其中,ℓ 为数据集X的长度(行数)。
最大化似然等价于最小化以下表达式:
上图体现了这样一个想法:如果不能够直接最小化分类问题的误差数量(至少无法通过梯度方法最小化,因为 0-1 损失函数在 0 的导数趋向无穷),那么可以转而最小化它的上界。
因此,可以通过降低分类误差数Llog的上限,降低分数误差数本身。
逻辑回归的L2正则化
逻辑回归正则化示例
我们使用吴恩达机器学习课程中的「微芯片测试」数据集,运用基于多项式特征的逻辑回归方法,然后改变正则化参数 CC。首先,看看正则化是如何影响分类器的分界,并查看欠拟合和过拟合的情况。接着,将通过交叉验证和网格搜索方法来选择接近最优值的正则化参数。
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV
from sklearn.preprocessing import PolynomialFeatures
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
这个数据集内有 118 个微芯片(目标),其中有两项质量控制测试的结果(两个数值变量)和微芯片是否投产的信息。变量已经过归一化,即列中的值已经减去其均值。所以,微芯片的平均测试值为零。
# 读取数据集
data = pd.read_csv('https://labfile.oss.aliyuncs.com/courses/1283/microchip_tests.txt',
header=None, names=('test1', 'test2', 'released'))
# 查看数据集的一些信息
data.info()
data.head(5)
data.tail(5)
分离训练集和目标分类标签。
X = data.iloc[:, :2].values
y = data.iloc[:, 2].values
绘制数据,橙点对应有缺陷的芯片,蓝点对应正常芯片。
plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=1')
plt.legend()
定义一个函数来显示分类器的分界线。
def plot_boundary(clf, X, y, grid_step=.01, poly_featurizer=None):
x_min, x_max = X[:, 0].min() - .1, X[:, 0].max() + .1
y_min, y_max = X[:, 1].min() - .1, X[:, 1].max() + .1
xx, yy = np.meshgrid(np.arange(x_min, x_max, grid_step),
np.arange(y_min, y_max, grid_step))
# 在 [x_min, m_max]x[y_min, y_max] 的每一点都用它自己的颜色来对应
Z = clf.predict(poly_featurizer.transform(np.c_[xx.ravel(), yy.ravel()]))
Z = Z.reshape(xx.shape)
plt.contour(xx, yy, Z, cmap=plt.cm.Paired)
特征的数量会呈指数型增长,为 100 个变量创建 d 较大(例如d=10)的多项式特征会导致计算成本变得很高。
使用 sklearn 库来实现逻辑回归。创建一个对象,为矩阵 X 加上多项式特征(d 不超过7)。
poly = PolynomialFeatures(degree=7)
X_poly = poly.fit_transform(X)
X_poly.shape
训练逻辑回归模型,正则化系数 C = 10^-2 。
C = 1e-2
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=.01, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()
print("Accuracy on training set:",
round(logit.score(X_poly, y), 3))
尝试减小正则化,即把 CC 增加到 1,现在的模型权重可以比之前有更大的值(绝对值更大),这使得分类器在训练集上的精确度提高到 0.831。
C = 1
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=.005, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()
print("Accuracy on training set:",
round(logit.score(X_poly, y), 3))
倘若继续增加 C到 10000 会如何?看下面结果,很明显正则化不够强导致了过拟合现象。
C = 1e4
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=.005, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c='blue', label='Released')
plt.scatter(X[y == 0, 0], X[y == 0, 1], c='orange', label='Faulty')
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title('2 tests of microchips. Logit with C=%s' % C)
plt.legend()
print("Accuracy on training set:",
round(logit.score(X_poly, y), 3))
正则化参数的调整
对上述例子中正则化参数 C 进行调参。使用 LogisticRegressionCV() 方法进行网格搜索参数后再交叉验证,LogisticRegressionCV() 是专门为逻辑回归设计的。如果想对其他模型进行同样的操作,可以使用 GridSearchCV() 或 RandomizedSearchCV() 等超参数优化算法。
# 该单元格执行时间较长,请耐心等待
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)
# 下方结尾的切片为了在线上环境搜索更快,线下练习时可以删除
c_values = np.logspace(-2, 3, 500)[50:450:50]
logit_searcher = LogisticRegressionCV(
Cs=c_values, cv=skf, verbose=1, n_jobs=-1)
logit_searcher.fit(X_poly, y)
logit_searcher.C_
查看超参数 C 是如何影响模型的质量的。
plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel('C')
plt.ylabel('Mean CV-accuracy')
最后,选择 C 值「最佳」的区域,即在 Mean CV-accuracy 值达到较大值的前提下选择较小的 C。上图由于 C 过大,无法辨认具体哪个较小的 C达到了较好的 Mean CV-accuracy 值,可以仅画出 C为 0 到 10 时的验证曲线。
plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel('C')
plt.ylabel('Mean CV-accuracy')
plt.xlim((0, 10))
上图可见,C=2时就达到了较好的 Mean CV-accuracy 值。
逻辑回归的优缺点
通过分析 IMDB 影评的二元分类问题和 XOR 问题来简要说明逻辑回归的优缺点。
分析 IMDB 二元分类问题
IMDB 数据集中的训练集包含标记好的影评,其中有 12500 条好评,12500 条差评。使用词袋模型构建输入矩阵 X ,语料库包含所有用户影评,影评的特征将由整个语料库中每个词的出现情况来表示。下图展示了这一思路:
import os
import numpy as np
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
载入 IMDB 数据集。首先,我们下载并解压数据。
# 文件较多,请耐心等待解压完成
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1283/aclImdb_v1.tar.gz"
!tar -zxvf "aclImdb_v1.tar.gz"
PATH_TO_IMDB = "aclImdb/"
reviews_train = load_files(os.path.join(PATH_TO_IMDB, "train"),
categories=['pos', 'neg'])
text_train, y_train = reviews_train.data, reviews_train.target
reviews_test = load_files(os.path.join(PATH_TO_IMDB, "test"),
categories=['pos', 'neg'])
text_test, y_test = reviews_test.data, reviews_test.target
看看训练集和测试集中各有多少条数据。
print("Number of documents in training data: %d" % len(text_train))
print(np.bincount(y_train))
print("Number of documents in test data: %d" % len(text_test))
print(np.bincount(y_test))
下面是该数据集中的一些影评。
text_train[1]
查看一下上面这条影评是差评还是好评。
y_train[1]
y_train=0 表示影评是差评,y_train=1 表示影评是好评,上面这条影片是差评。
单词的简单计数
首先,使用 CountVectorizer() 创建包含所有单词的字典。
cv = CountVectorizer()
cv.fit(text_train)
len(cv.vocabulary_)
查看创建后的「单词」样本,发现 IMDB 数据集已经自动进行了文本处理
print(cv.get_feature_names()[:50])
print(cv.get_feature_names()[50000:50050])
接着,使用单词的索引编码训练集的句子,用稀疏矩阵保存。
X_train = cv.transform(text_train)
X_train
看看上述转换过程是如何进行的,首先查看需要转换的训练集句子。
text_train[19726]
然后将每个单词转换成对应的单词索引。
X_train[19726].nonzero()[1]
X_train[19726].nonzero()
接下来,对测试集应用同样的操作。
X_test = cv.transform(text_test)
之后就可以使用逻辑回归来训练模型。
logit = LogisticRegression(solver='lbfgs', n_jobs=-1, random_state=7)
logit.fit(X_train, y_train)
训练完成后,查看训练集和测试集上的准确率(Accuracy)。
round(logit.score(X_train, y_train), 3), round(logit.score(X_test, y_test), 3),
可视化模型的系数。
def visualize_coefficients(classifier, feature_names, n_top_features=25):
# get coefficients with large absolute values
coef = classifier.coef_.ravel()
positive_coefficients = np.argsort(coef)[-n_top_features:]
negative_coefficients = np.argsort(coef)[:n_top_features]
interesting_coefficients = np.hstack(
[negative_coefficients, positive_coefficients])
# plot them
plt.figure(figsize=(15, 5))
colors = ["red" if c < 0 else "blue" for c in coef[interesting_coefficients]]
plt.bar(np.arange(2 * n_top_features),
coef[interesting_coefficients], color=colors)
feature_names = np.array(feature_names)
plt.xticks(np.arange(1, 1 + 2 * n_top_features),
feature_names[interesting_coefficients], rotation=60, ha="right")
def plot_grid_scores(grid, param_name):
plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_train_score'],
color='green', label='train')
plt.plot(grid.param_grid[param_name], grid.cv_results_['mean_test_score'],
color='red', label='test')
plt.legend()
visualize_coefficients(logit, cv.get_feature_names())
对逻辑回归的正则化系数进行调参。make_pipeline() 确保的序列顺序,在训练数据上应用 CountVectorizer() 方法,然后训练逻辑回归模型。
from sklearn.pipeline import make_pipeline
# 该单元格执行时间较长,请耐心等待
text_pipe_logit = make_pipeline(CountVectorizer(),
LogisticRegression(solver='lbfgs',
n_jobs=1,
random_state=7))
text_pipe_logit.fit(text_train, y_train)
print(text_pipe_logit.score(text_test, y_test))
from sklearn.model_selection import GridSearchCV
# 该单元格执行时间较长,请耐心等待
param_grid_logit = {'logisticregression__C': np.logspace(-5, 0, 6)[4:5]}
grid_logit = GridSearchCV(text_pipe_logit,
param_grid_logit,
return_train_score=True,
cv=3, n_jobs=-1)
grid_logit.fit(text_train, y_train)
查看一下最佳 C,以及相应的交叉验证评分。
grid_logit.best_params_, grid_logit.best_score_
plot_grid_scores(grid_logit, 'logisticregression__C')
调优后的逻辑回归模型在验证集上的准确率。
grid_logit.score(text_test, y_test)
换一种方法,使用随机森林来分类。
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=200,
n_jobs=-1, random_state=17)
forest.fit(X_train, y_train)
round(forest.score(X_test, y_test), 3)
上述结果可见,相较于随机森林,逻辑回归在 IMDB 数据集上表现更优。
XOR 问题
线性分类定义的是一个非常简单的分界平面:一个超平面,这导致线性模型在 XOR 问题上表现不佳。XOR 即异或,其真值表如下:
XOR 是一个简单的二元分类问题,其中两个分类呈对角交叉分布。下面创建数据集。
rng = np.random.RandomState(0)
X = rng.randn(200, 2)
y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0)
plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
显然,无法划出一条直线无误差地将两个分类分开。因此,逻辑回归在这一任务上的表现很差。
def plot_boundary(clf, X, y, plot_title):
xx, yy = np.meshgrid(np.linspace(-3, 3, 50),
np.linspace(-3, 3, 50))
clf.fit(X, y)
# plot the decision function for each datapoint on the grid
Z = clf.predict_proba(np.vstack((xx.ravel(), yy.ravel())).T)[:, 1]
Z = Z.reshape(xx.shape)
image = plt.imshow(Z, interpolation='nearest',
extent=(xx.min(), xx.max(), yy.min(), yy.max()),
aspect='auto', origin='lower', cmap=plt.cm.PuOr_r)
contours = plt.contour(xx, yy, Z, levels=[0], linewidths=2,
linetypes='--')
plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
plt.xticks(())
plt.yticks(())
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$')
plt.axis([-3, 3, -3, 3])
plt.colorbar(image)
plt.title(plot_title, fontsize=12)
plot_boundary(LogisticRegression(solver='lbfgs'), X, y,
"Logistic Regression, XOR problem")
然而,如果将输入变为多项式特征(这里 dd = 2),那么这一任务就可以得到较好的解决。
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
logit_pipe = Pipeline([('poly', PolynomialFeatures(degree=2)),
('logit', LogisticRegression(solver='lbfgs'))])
plot_boundary(logit_pipe, X, y,
"Logistic Regression + quadratic features. XOR problem")
在实际应用中,多项式特征确实有用,不过显式的创建它们会大大提升计算复杂度。使用核(kernel)函数的 SVM 方法相较逻辑回归要快很多,在 SVM 中,只计算高维空间中目标之间的距离(由核函数定义),而不用生成大量特征组合。
验证和学习曲线
import numpy as np
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV, SGDClassifier
from sklearn.model_selection import validation_curve, learning_curve
from matplotlib import pyplot as plt
考虑一个更大的问题:如果模型的质量不佳,该怎么办?针对这个问题,有很多猜想:
应该让模型更复杂还是更简单?
应该加入更多特征吗?
是否只是需要更多数据用于训练?
这些猜想的答案并不明显,比如有时候一个更复杂的模型会导致表现退化,有时候增加新的特征带来的变化并不直观。事实上,做出正确决定,选择正确方法,从而改进模型的能力是衡量一个人对机器学习知识掌握程度的重要指标。
看看电信运营商的客户离网数据集。
data = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv').drop('State', axis=1)
data['International plan'] = data['International plan'].map(
{'Yes': 1, 'No': 0})
data['Voice mail plan'] = data['Voice mail plan'].map({'Yes': 1, 'No': 0})
y = data['Churn'].astype('int').values
X = data.drop('Churn', axis=1).values
使用随机梯度下降训练逻辑回归(在之后的实验中将会专门讨论梯度下降)。
alphas = np.logspace(-2, 0, 20)
sgd_logit = SGDClassifier(loss='log', n_jobs=-1, random_state=17, max_iter=5)
logit_pipe = Pipeline([('scaler', StandardScaler()), ('poly', PolynomialFeatures(degree=2)),
('sgd_logit', sgd_logit)])
val_train, val_test = validation_curve(logit_pipe, X, y,
'sgd_logit__alpha', alphas, cv=5,
scoring='roc_auc')
绘制 ROC-AUC 曲线,查看不同正则化参数下模型在训练集和测试集上的表现有何不同。
def plot_with_err(x, data, **kwargs):
mu, std = data.mean(1), data.std(1)
lines = plt.plot(x, mu, '-', **kwargs)
plt.fill_between(x, mu - std, mu + std, edgecolor='none',
facecolor=lines[0].get_color(), alpha=0.2)
plot_with_err(alphas, val_train, label='training scores')
plot_with_err(alphas, val_test, label='validation scores')
plt.xlabel(r'$\alpha$')
plt.ylabel('ROC AUC')
plt.legend()
plt.grid(True)
上图的趋势表明:
简单模型的训练误差和验证误差很接近,且都比较大。这暗示模型欠拟合,参数数量不够多。
高度复杂模型的训练误差和验证误差相差很大,这暗示模型过拟合。当参数数量过多或者正则化不够严格时,算法可能被数据中的噪声「转移注意力」,没能把握数据的整体趋势。
数据对于模型的影响
一般而言,模型所用的数据越多越好。但新数据是否在任何情况下都有帮助呢?例如,为了评估特征 N ,而对数据集的数据进行加倍,这样做是否合理?
由于新数据可能难以取得,合理的做法是改变训练集的大小,然后看模型的质量与训练数据的数量之间的依赖关系,这就是「学习曲线」的概念。
这个想法很简单:将误差看作训练中所使用的样本数量的函数。模型的参数事先固定。
def plot_learning_curve(degree=2, alpha=0.01):
train_sizes = np.linspace(0.05, 1, 20)
logit_pipe = Pipeline([('scaler', StandardScaler()), ('poly', PolynomialFeatures(degree=degree)),
('sgd_logit', SGDClassifier(n_jobs=-1, random_state=17, alpha=alpha, max_iter=5))])
N_train, val_train, val_test = learning_curve(logit_pipe,
X, y, train_sizes=train_sizes, cv=5,
scoring='roc_auc')
plot_with_err(N_train, val_train, label='training scores')
plot_with_err(N_train, val_test, label='validation scores')
plt.xlabel('Training Set Size')
plt.ylabel('AUC')
plt.legend()
plt.grid(True)
把正则化系数设定为较大的数(alpha=10),查看线性模型的表现情况。
plot_learning_curve(degree=2, alpha=10)
上图表明:对于少量数据而言,训练集和交叉验证集之间的误差差别(方差)相当大,这暗示了过拟合。同样的模型,使用大量数据,误差「收敛」,暗示了欠拟合。加入更多数据,该训练集的误差不会增加,且该验证集上的误差也不会下降。所以,倘若误差「收敛」,如果不改变模型的复杂度,而是仅仅把数据集大小增大 10 倍,或许对最终的表现结果没有太大帮助。
如果将正则化系数 alpha 降低到 0.05,会怎么样?
plot_learning_curve(degree=2, alpha=0.05)
上图表明,降低正则化系数 alpha 至 0.05,曲线将逐渐收敛,如果加入更多数据,可以进一步改善模型在验证集上的表现。
如果把 alpha 设为 10^-4,让模型更复杂,会出现什么情况?
plot_learning_curve(degree=2, alpha=1e-4)
上图表明,与正则化系数 alpha=0.05 相比,在训练集和验证集上,AUC 都下降了,出现过拟合现象。
构建学习曲线和验证曲线可以帮助我们为新数据调整合适的模型复杂度。
关于验证曲线和学习曲线的结论:
训练集上的误差本身不能说明模型的质量。
交叉验证误差除了可以显示模型对数据的拟合程度外,还可以显示模型保留了多少对新数据的概括能力。
验证曲线是一条根据模型复杂度显示训练集和验证集结果的曲线:如果两条曲线彼此接近,且两者的误差都很大,这标志着欠拟合;如果两条曲线彼此距离很远,这标志着过拟合。
学习曲线是一个根据观测数量显示训练集和验证集结果的曲线:如果两条曲线收敛,那么增加新数据收益不大,有必要改变模型复杂度;如果两条曲线没有收敛,增加新数据可以改善结果。
逻辑回归用于讽刺文本检测
逻辑回归用于讽刺文本检测
该语料数据来源于 Reddit 论坛,通过下面的链接下载并解压数据:
!wget -nc "http://labfile.oss.aliyuncs.com/courses/1283/train-balanced-sarcasm.csv.zip"
!unzip -o "train-balanced-sarcasm.csv.zip"
首先,导入挑战所需的必要模块。
import os
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
import seaborn as sns
from matplotlib import pyplot as plt
import warnings
warnings.filterwarnings('ignore')
然后,加载语料并预览。
train_df = pd.read_csv('train-balanced-sarcasm.csv')
train_df.head()
查看数据集变量类别信息。
train_df.info()
comment 的数量小于其他特征数量,说明存在缺失值。这里直接将这些缺失数据样本删除。
train_df.dropna(subset=['comment'], inplace=True)
输出数据标签,看一看类别是否平衡。
train_df['label'].value_counts()
最后,将数据切分为训练和测试集。
train_texts, valid_texts, y_train, y_valid = \
train_test_split(train_df['comment'], train_df['label'], random_state=17)
数据可视化探索
首先,使用条形图可视化讽刺和正常文本长度,这里利用 np.log1p 对数据进行平滑处理,压缩到一定区间范围内。
train_df.loc[train_df['label'] == 1, 'comment'].str.len().apply(
np.log1p).hist(label='sarcastic', alpha=.5)
train_df.loc[train_df['label'] == 0, 'comment'].str.len().apply(
np.log1p).hist(label='normal', alpha=.5)
plt.legend()
可以看的,二者在不同长度区间范围(横坐标)的计数分布比较均匀。接下来,挑战需要利用 WordCloud 绘制讽刺文本和正常文本关键词词云图。
制两类评论文本词云图,可自定义样式效果。
from wordcloud import WordCloud, STOPWORDS
wordcloud = WordCloud(background_color='black', stopwords=STOPWORDS,
max_words=200, max_font_size=100,
random_state=17, width=800, height=400)
plt.figure(figsize=(16, 12))
wordcloud.generate(str(train_df.loc[train_df['label'] == 1, 'comment']))
plt.imshow(wordcloud)
plt.figure(figsize=(16, 12))
wordcloud.generate(str(train_df.loc[train_df['label'] == 0, 'comment']))
plt.imshow(wordcloud)
词云非常好看,但往往看不出太多有效信息。
subreddit 表示评论归属于 Reddit 论坛子板块信息。下面,我们使用 groupby 来确定各子板块讽刺评论数量排序。
上面的代码中,np.size 可以计算出不同子板块评论的总数。由于讽刺评论的标签为 1,正常评论为 0,所以通过 sum 求和操作就可以直接求出讽刺评论的计数。同理,mean 即代表讽刺评论所占比例。
沿用以上数据,输出子板块评论数大于 1000 且讽刺评论比例排名前 10 的信息。
sub_df[sub_df['size'] > 1000].sort_values(by='mean', ascending=False).head(10)
同理,可以从用户的维度去分析讽刺评论的比例分布。下面就需要分析得出不同用户 author 发表评论的数量、讽刺评论的数量及比例。
输出发表评论总数大于 300,且讽刺评论比例最高的 10 位用户信息。
sub_df = train_df.groupby('author')['label'].agg([np.size, np.mean, np.sum])
sub_df[sub_df['size'] > 300].sort_values(by='mean', ascending=False).head(10)
训练分类模型
接下来,我们训练讽刺评论分类预测模型。这里,我们使用 tf-idf 提取文本特征,并建立逻辑回归模型。
# 使用 tf-idf 提取文本特征
tf_idf = TfidfVectorizer(ngram_range=(1, 2), max_features=50000, min_df=2)
# 建立逻辑回归模型
logit = LogisticRegression(C=1, n_jobs=4, solver='lbfgs',
random_state=17, verbose=1)
# 使用 sklearn pipeline 封装 2 个步骤
tfidf_logit_pipeline = Pipeline([('tf_idf', tf_idf),
('logit', logit)])
训练讽刺文本分类预测模型,并得到测试集上的准确度评估结果。
tfidf_logit_pipeline.fit(train_texts, y_train)
valid_pred = tfidf_logit_pipeline.predict(valid_texts)
accuracy_score(y_valid, valid_pred)
模型解释
接下来,挑战构建一个混淆矩阵的函数 plot_confusion_matrix。
def plot_confusion_matrix(actual, predicted, classes,
normalize=False,
title='Confusion matrix', figsize=(7, 7),
cmap=plt.cm.Blues, path_to_save_fig=None):
"""
This function prints and plots the confusion matrix.
Normalization can be applied by setting `normalize=True`.
"""
import itertools
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(actual, predicted).T
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
plt.figure(figsize=figsize)
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=90)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.tight_layout()
plt.ylabel('Predicted label')
plt.xlabel('True label')
if path_to_save_fig:
plt.savefig(path_to_save_fig, dpi=300, bbox_inches='tight')
应用 plot_confusion_matrix 绘制出测试数据原始标签和预测标签类别的混淆矩阵。
plot_confusion_matrix(y_valid, valid_pred,
tfidf_logit_pipeline.named_steps['logit'].classes_, figsize=(8, 8))
利用 eli5 可以输出分类器在预测判定是文本特征的权重。
import eli5
eli5.show_weights(estimator=tfidf_logit_pipeline.named_steps['logit'],
vec=tfidf_logit_pipeline.named_steps['tf_idf'])
我们可以发现,讽刺评论通常都喜欢使用 yes, clearly 等带有肯定意味的词句。
模型改进
再补充一个 subreddit 特征,同样完成切分。注意,这里切分时一定要选择同一个 random_state,保证能和上面的评论数据对齐。
subreddits = train_df['subreddit']
train_subreddits, valid_subreddits = train_test_split(
subreddits, random_state=17)
接下来,同样使用 tf-idf 算法分别构建 2 个 TfidfVectorizer 用于 comment 和 subreddits 的特征提取。
tf_idf_texts = TfidfVectorizer(
ngram_range=(1, 2), max_features=50000, min_df=2)
tf_idf_subreddits = TfidfVectorizer(ngram_range=(1, 1))
使用构建好的 TfidfVectorizer 完成特征提取。
X_train_texts = tf_idf_texts.fit_transform(train_texts)
X_valid_texts = tf_idf_texts.transform(valid_texts)
X_train_texts.shape, X_valid_texts.shape
X_train_subreddits = tf_idf_subreddits.fit_transform(train_subreddits)
X_valid_subreddits = tf_idf_subreddits.transform(valid_subreddits)
X_train_subreddits.shape, X_valid_subreddits.shape
然后,将提取出来的特征拼接在一起。
from scipy.sparse import hstack
X_train = hstack([X_train_texts, X_train_subreddits])
X_valid = hstack([X_valid_texts, X_valid_subreddits])
X_train.shape, X_valid.shape
最后,同样使用逻辑回归进行建模和预测。
使用新特征训练逻辑回归分类模型并得到测试集上的分类准确度。
logit.fit(X_train, y_train)
valid_pred = logit.predict(X_valid)
accuracy_score(y_valid, valid_pred)