统计机器学习相关

1、请你说说对于聚类算法的了解

什么是聚类算法

  • 聚类(Clustering)是无监督学习(unsupervisied learning),即不需要标签。
  • 聚类是按照某个指标(如样本间的距离)把一个整个数据集分割成不同的类或簇(cluster),使类内元素的相似性尽可能大,类间元素的相似性尽可能地小
  • 简单来说,聚类使同一类的数据尽可能聚集到一起,不同类数据尽量分离。

聚类的一般步骤

  1. 数据准备:特征标准化(白化,whiting)
  2. 特征选择:特征降维,选择最有效的特征
  3. 特征提取:对选择的特征进行转换,提取出更有代表性的特征
  4. 聚类:基于特定的度量函数进行相似度度量,使同一类的数据尽可能聚集到一起,不同类数据尽量分离,得到各个簇的中心,以及每个元素的类标签
  5. 评估:分析聚类结果,如距离误差和(SSE)等

常用聚类算法有哪些、对应的度量函数分别是

聚类算法
Geometry (metric used)
K-Means(K-均值)
Distances between points(点之间的距离)
Mean-shift
Distances between points(点之间的距离)
Spectral clustering
Graph distance (e.g. nearest-neighbor graph)(图距离(例如最近邻图)
Ward hierarchical clustering
Distances between points(点之间的距离)
DBSCAN
Distances between nearest points(最近点之间的距离)
Gaussian mixtures(高斯混合)
Mahalanobis distances to centers( 与中心的马氏距离)

聚类跟分类的本质区别

聚类是无监督,分类是有监督

延伸考点

本题问的比较宽泛,可以自由发挥,只挑自己熟悉的回答;
面试官往往会接着往下问,比如“挑一个你最熟悉的,详细介绍一下吧”,或者“讲讲 kmeans 吧”,所以需要至少深入了解一个算法(推荐 kmeans)

2、知道kmeans吧?说下迭代过程,簇心随机不好,怎么才能更稳定?

k-means算法,也被称为k-均值算法,是最基本也是应用最广泛的聚类算法。该算法的核心思想是将各个聚类子集内的所有数据样本的均值作为该聚类的代表点。
即在迭代过程中把数据集划分为不同的类别,使得评价聚类性能的距离函数达到最优,从而使生成的每个聚类内紧凑,类间远离。

迭代过程

输入:簇的数目k和包含n个对象的数据库。
输出:k个簇,使平方误差准则最小。
算法步骤:
  1. 为每个聚类确定一个初始聚类中心,这样就有K 个初始聚类中心。
  2. 将样本集中的样本按照最小距离原则分配到最邻近聚类
  3. 使用每个聚类中的样本均值作为新的聚类中心。
  4. 重复步骤2.3直到聚类中心不再变化。
  5. 结束,得到K个聚类

伪代码

input: 获取数据 n 个 m 维的数据
input: 随机生成 K 个 m 维的点
while(t)  // t 表示设置的终止条件,比如迭代次数
    for(int i=0;i < n;i++)  // 遍历每个元素
        for(int j=0;j < k;j++)  // 为每个元素计算其到 K 个聚类中心的距离
            计算点 i 到类 j 的距离
    for(int i=0;i < k;i++)  // 更新聚类中心点
        1. 找出所有属于自己这一类的所有数据点  // 通过选定的距离度量函数确定
        2. 把自己的坐标修改为这些数据点的中心点坐标
end

output: K 个 m 维的点(聚成了K类)


复杂度分析

1、时间复杂度
O(tnkm),其中, t 为迭代次数,k 为簇的数目,n 为样本点数,m 为样本点维度。
2、空间复杂度
O(m(n+k)),其中,k 为簇的数目,m 为样本点维度,n 为样本点数。

k-means 迭代过程可视化

对 kmeans 算法不熟悉的同学可以从该网站里面,直观地了解kmeans 迭代的每个过程

簇心随机不好,怎么才能更稳定

K-Means算法试图找到使平方误差准则函数最小的簇,当潜在的簇形状是凸面的,簇与簇之间区别较明显,且簇大小相近时,其聚类结果较理想,但对于其他场景效果则不一定很好。
而且该算法除了要事先确定簇数K和对初始聚类中心敏感外,经常以局部最优结束,同时对“噪声”和孤立点敏感,并且该方法不适于发现非凸面形状的簇或大小差别很大的簇。
举个例子,典型的月牙形数据
from sklearn.datasets import make_moons
X, y = make_moons(200, noise=.05, random_state=0)

labels = KMeans(2, random_state=0).fit_predict(X)
plt.scatter(X[:, 0], X[:, 1], c=labels,  s=50, cmap='viridis')
解决这种场景的思路可以借鉴支持向量机的核函数(使用核函数转换来将数据投影成到更高维度),在聚类中,可以考虑谱聚类,即Spectral clustering。
from sklearn.cluster import SpectralClustering
model = SpectralClustering(n_clusters=2, affinity='nearest_neighbors', assign_labels='kmeans')
labels = model.fit_predict(X)
plt.scatter(X[:, 0], X[:, 1], c=labels, s=50, cmap='viridis')

总结来说,主要有三种方法,解决簇心随机不好的问题,使聚类更稳定
  • 多次运行,每次使用一组不同的随机初始质心,然后选取具有最小SSE(误差的平方和)的簇集。这种策略简单,但是效果可能不好(本质还是随机选取)。
  • 取一个样本,并使用谱聚类或者层次聚类技术对它聚类。比如从层次聚类中提取K个簇,并用这些簇的质心作为初始质心。
  • 取所有点的质心作为第一个点,然后,对于每个后继初始质心,选择离已经选取过的初始质心最远的点。使用这种方法,确保了选择的初始质心不仅是随机的,而且是散开的。但是,这种方法可能选中离群点。(典型的改进是 kmeans++)

延伸考点

本题考察 kmeans 算法的基础知识,是聚类算法里最常考的考点;
此外,还可能延伸考察 对kmeans 法的缺点(即随机选择簇心导致聚类不稳定),以及典型的改进(kmeans++),对应前文的第三点:选择离已选中心点最远的点,从而保证聚类结果“类内相似、类间远离”。
ps: 网易云课堂上有斯坦福大学的机器学习的公开课,有空可以看看。

3、kmeans聚类如何选择初始点?

首先看一下 k means 聚类常用的度量函数
1、欧式距离(Euclidean distance)
d_{e u c}(x, y)=\sqrt{\sum_{i=1}^{n}\left(x_{i}-y_{i}\right)^{2}}
2、曼哈顿距离(Manhattan distance)
d_{m a n}(x, y)=\sum_{i=1}^{n}\left|\left(x_{i}-y_{i}\right)\right|
其中 x,y 都是维度为 n 的向量。

kmeans 算法的核心步骤如下:
  1. 预定义一些聚类中心
  2. 重复以下步骤,直到收敛
    1)把每个点归类到离他最近的聚类中心(依据前面提到的度量函数)
    2)计算新的聚类中心
第一步“预定义一些聚类中心”,即为如何选择初始点,该步骤将影响整个kmeans 算法的性能。

选择聚类初始点,包括两块内容:
  1. 确定聚类初始点的个数
  2. 确定聚类初始点的位置

确定聚类初始点的个数

先看第一步,聚类是无监督算法,也就是说我们没有样本的标签,事先无从得知样本的类别数,以下面这个样本为例,直观上我们应该设置类别数为 3。
下面会给出本示例的代码
但事先我们无从得知,可能设置为 6,那么聚类结果就是
为此,研究人员提出了一个叫“手肘法 elbow method”的方法,简单来说就是遍历所有可能的聚类中心点数,画出在该点数下的 loss 大小,选择拐点的位置。
基于sklearn 算法库,可以很方便地使用
distortions = []
K = range(1,10)
# 遍历所有可能的聚类中心点数
for k in K:
    kmeanModel = KMeans(n_clusters=k)
    kmeanModel.fit(df)
    distortions.append(kmeanModel.inertia_)  # 统计在该点数下的 loss 大小

plt.figure(figsize=(16,8))
plt.plot(K, distortions, 'bx-')
plt.xlabel('k')
plt.ylabel('Distortion')
plt.title('The Elbow Method showing the optimal k')
plt.show()

从手肘法中,可以看出拐点在 2 或者 3,所以聚类初始点的个数应该选择 2 或者 3

确定聚类初始点的位置

聚类初始点的位置,对算法性能影响也很大,对于不同的初始值,可能会导致不同结果。
而且它对于“躁声”和孤立点数据是敏感的,少量的该类数据能够对平均值产生极大的影响。
所以,确定初始点位置的方法主要包括:
  1. 随机确定初始点的位置
  2. 多设置一些不同的初值,对比最后的运算结果,一直到结果趋于稳定结束,但比较耗时和浪费资源
  3. 以 K-means++为代表的改进算法
    1. 随机选取一个中心点
    2. 计算数据到之前 n 个聚类中心最远的距离 ,并以一定概率随机选择新中心点 ;
    3. 重复第二步。
简单的来说,就是 K-means++ 就是选择离已选中心点最远的点,从而保证聚类结果“类内相似、类间远离

延伸考点

kmeans 及 kmeans++都是经典的聚类算法,考察基本功,建议掌握,并通过示例代码学习。
另外,需要注意 kmeans 与 knn 的区别
K-Means是无监督学习的聚类算法,没有样本输出;而KNN是监督学习的分类算法,有对应的类别输出。
当然,两者也有一些相似点,两个算法都包含一个过程,即找出和某一个点最近的点。两者都利用了最近邻(nearest neighbors)的思想。

4、请你介绍一下SVM

SVM,Support Vector Machine,也即是支持向量机,基本原理是求解能够正确划分训练数据集并且几何间隔最大的分离超平面
如下图所示,我们以最简单的线性可分的数据为例:
想要分开左上角和右下角的两类数据,两条虚线上的点即为“支持向量”,也就是两类数据的边界;在两条虚线之间,有无数条线可以把两类数据分开,但最中间的那条实线,是最理想的情况:几何边界最大的分离超平面
但需要强调的是,svm 不仅可以支持这种简单的线性可分离的数据,还可以
  1. 借助“软间隔(soft margin)”实现对不能线性可分但是可以近似线性可分的数据的分类
  2. 借助“核函数”,或者说“核技巧 kernel tirck”,实现对任意非线性数据的分类


公式推导

代码示例

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
import seaborn as sns
sns.set()

# 生成示例数据
from sklearn.datasets import make_blobs
X, y = make_blobs(n_samples=50, centers=2,
                  random_state=0, cluster_std=0.60)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plt.show()
# 手动选择超平面
xfit = np.linspace(-1, 3.5)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')

for m, b, d in [(1, 0.65, 0.33), (0.5, 1.6, 0.55), (-0.2, 2.9, 0.2)]:
    yfit = m * xfit + b
    plt.plot(xfit, yfit, '-k')
    plt.fill_between(xfit, yfit - d, yfit + d, edgecolor='none',
                     color='#AAAAAA', alpha=0.4)

plt.xlim(-1, 3.5)
plt.show()
# 使用 svm 实现
from sklearn.svm import SVC # "Support vector classifier"
model = SVC(kernel='linear', C=1E10)
model.fit(X, y)

# 可视化
def plot_svc_decision_function(model, ax=None, plot_support=True):
    """绘制二维SVC的决策函数"""
    if ax is None:
        ax = plt.gca()
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    x = np.linspace(xlim[0], xlim[1], 30)
    y = np.linspace(ylim[0], ylim[1], 30)
    Y, X = np.meshgrid(y, x)
    xy = np.vstack([X.ravel(), Y.ravel()]).T
    P = model.decision_function(xy).reshape(X.shape)

    # 绘制决策边界和边缘
    ax.contour(X, Y, P, colors='k',
               levels=[-1, 0, 1], alpha=0.5,
               linestyles=['--', '-', '--'])

    # 绘制支持向量
    if plot_support:
        ax.scatter(model.support_vectors_[:, 0],
                   model.support_vectors_[:, 1],
                   s=300, linewidth=1, facecolors='none');
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
    plt.show()


plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(model)

延伸考点

本题考察对 svm算法的了解,该算法是经典的机器学习算法,但往往不会考察公式推导(需要对凸优化有比较深入的了解),只需要大致讲讲算法原理即可,所以准备面试时,也不需要浪费时间在公式推导上。

5、请你说说SVM的优缺点

优点
缺点
解决小样本下机器学习问题(不像深度学习一样,依赖海量数据)
常规SVM只支持二分类
可以解决高维问题,即大型特征空间(借助核函数)
泛化能力比较强
无需依赖整个数据
对缺失数据敏感
无局部极小值问题
能够处理非线性特征的相互作用
对非线性问题没有通用解决方案,有时候很难找到一个合适的核函数
SVM 的最终决策函数只由少数的支持向量所确定,计算的复杂性取决于支持向量的数目,而不是样本空间的维数,这在某种意义上避免了“维数灾难”。
当样本很多时,效率并不是很高(借助二次规划来求解支持向量,而求解二次规划将涉及m阶矩阵的计算(m为样本的个数),当m数目很大时该矩阵的存储和计算将耗费大量的机器内存和运算时间)
理论基础扎实,分类问题可以从原理上解释(相比于“玄学调参”的深度学习)
对于核函数的高维映射可解释性不强,尤其是径向基函数

6、请你说说RF和SVM的特点,评价

支持向量机(SVM)已经介绍很多了,主要讲讲随机森林(random forest, RF)。
想要了解随机森林,首先要知道决策树,即森林由一棵棵树组成。

决策树

决策树是一种有监督的机器学习算法,该方法可以用于解决分类和回归问题。
决策树可以简单地理解为达到某一特定结果的一系列决策。思考逻辑上,就像一连串的if-else,如果满足 xx 特征,则归为 xx 类别,否则则归为 yy 类别。(可以参考周志华老师《机器学习》里挑西瓜的案例)
这其中的关键,就是如何选取特征。一棵树能选取的特征往往有限,限制了模型的性能。因此就有了随机森林。

随机森林

随机森林是基于决策树的机器学习算法,该算法利用了多棵决策树的力量来进行决策。
为什么要称其为“随机森林”呢?这是因为它是随机创造的决策树组成的森林。决策树中的每一个节点是特征的一个随机子集,用于计算输出。随机森林将单个决策树的输出整合起来生成最后的输出结果。
简单来说:“随机森林算法用多棵(随机生成的)决策树来生成最后的输出结果。
对于一个测试数据,将它投入到随机森林中的不同决策树中,会得到不同的测试结果。若问题是一个分类问题,则可以通过求众数来得出测试数据的类别;若问题是一个回归问题,则可以通过求平均值得出测试数据的值。该过程即为经典的 bagging思想
那么RF 和 SVM 的特点就可以归纳为
SVM
RF
解决小样本下机器学习问题(不像深度学习一样,依赖海量数据)
简单,容易实现,计算开销小,并且它在很多现实任务中展现出来了强大的性能
可以解决高维问题,即大型特征空间(借助核函数);
但当样本很多时,效率并不是很高
它能够处理很高维度(特征很多)的数据,并且不用做特征选择(可以随机选择各种特征)
SVM 的最终决策函数只由少数的支持向量所确定,计算的复杂性取决于支持向量的数目,而不是样本空间的维数
在训练完后,它能够给出哪些feature比较重要
无需依赖整个数据,无局部极小值问题;
但SVM算法对大规模训练样本难以实施
训练速度快,容易做成并行化方法
能够处理非线性特征的相互作用,对于核函数的高维映射可解释性不强,尤其是径向基函数
在创建随机森林的时候,对generlization error使用的是无偏估计,模型泛化能力强
对于不平衡的数据集来说,它可以平衡误差;
如果有很大一部分的特征遗失,仍可以维持准确度。

RF 代码示例

# 生成数据
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
import numpy as np

X, y = make_blobs(n_samples=300, centers=4,
                  random_state=0, cluster_std=1.0)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='rainbow')
plt.show()
使用随机森林分类,并可视化
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier


def visualize_classifier(model, X, y, ax=None, cmap='rainbow'):
    ax = ax or plt.gca()

    # 绘制训练数据
    ax.scatter(X[:, 0], X[:, 1], c=y, s=30, cmap=cmap,
               clim=(y.min(), y.max()), zorder=3)
    ax.axis('tight')
    ax.axis('off')
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()


    model.fit(X, y)
    xx, yy = np.meshgrid(np.linspace(*xlim, num=200),
                         np.linspace(*ylim, num=200))
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

    # 绘制结果
    n_classes = len(np.unique(y))
    contours = ax.contourf(xx, yy, Z, alpha=0.3,
                           levels=np.arange(n_classes + 1) - 0.5,
                           cmap=cmap, clim=(y.min(), y.max()),
                           zorder=1)

    ax.set(xlim=xlim, ylim=ylim)
    plt.show()


tree = DecisionTreeClassifier()
bag = BaggingClassifier(tree, n_estimators=100, max_samples=0.8,
                        random_state=1)

bag.fit(X, y)
visualize_classifier(bag, X, y)


7、LR和SVM的联系与区别

简单介绍下线性回归模型(LR)

Linear Regression,线性回归模型,是最基本的机器学习模型之一,要做的事情也很简单,预测一个线性函数,比如
y = a*x + b
其中 a 是斜率,b 是截距,LR 要做的就是预测斜率a和截距b。
优化的目标就是使预测值y^hat尽可能地接近真实值y,即
\text { error }=y-\hat{y}
下面给个简单的例子,目标函数是y = 2 * x - 5
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = 2 * x - 5 + rng.randn(50)  # 添加噪声,模拟真实场景
plt.scatter(x, y)

model = LinearRegression(fit_intercept=True)
model.fit(x[:, np.newaxis], y)

xfit = np.linspace(0, 10, 1000)
yfit = model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit)
plt.show()

LR 与 SVM的对比

LR
SVM
相同点
都可以处理分类问题
一般都用于处理线性二分类问题
改进后可以处理多分类问题
比如训练n个分类器,表示是或不是这个类别的概率,最终选择概率最大的作为最终类。
都是线性模型(基本的情况,不使用核函数)
都可以用来做非线性分类(加核函数)
都是判别模型
不同点
参数模型,经验风险最小化
非参数模型,结构风险最小化
LR的计算和所有点都有关系
不直接依赖数据分布,只与支持向量那几个点有关系
对数损失函数
Hinge 损失函数
输出具有自然的概率意义
输出不具有概率意义
模型简单,复杂度低,适合大数据
计算复杂,在小数据集上效果一般比LR 更好

延伸考点

有哪些常见的判别模型?
提示,线性回归模型、线性判别分析、支持向量机SVM、神经网络等

8、LR不使用MSE而使用交叉熵损失函数的原因

  • CrossEntropy, 交叉熵
  • MeanSquareError,均方误差
简单的回答就是:用MSE容易出现多个局部最优解(即非凸函数),这样很难找到全局最优训练出好的模型,也就是说模型会依赖初始权值的起点。
反之,用交叉熵就很容易找到全局最优,因为是代价函数是大部分情况是凸函数。另外, 用sigmoid是为了限制输出0~1范围内,从而具有概率的意义。
LR问题的基本形式(即 sigmoid 函数)
h_{\theta}(x)=g\left(\theta^{T} x\right)=\frac{1}{1+e^{-\theta^{T} x}},其中\theta ^ T x为模型表达式,比如前面提到的一元一次方程y=ax+b
  • 如果使用MSE作为损失函数
\begin{gathered}loss 计算公式:<br />C=\frac{(y-\hat{y})^{2}}{2} \\梯度更新公式:<br />\frac{\partial C}{\partial w}=(\hat{y}-y) \sigma^{\prime}(z)(x)<br />\end{gathered}
  • 如果使用交叉熵损失函数
\begin{gathered} loss计算公式:<br />C=\frac{1}{n} \sum[y \operatorname{In} \hat{y}+(1-y) \operatorname{In}(1-\hat{y})] \\<br />梯度更新公式:<br />\frac{\partial C}{\partial w}=\frac{1}{n} \sum x(\sigma(z)-y)<br />\end{gathered}
可以看出,使用MSE作为损失函数的话,它的梯度是和sigmod函数的导数有关的,如果当前模型的输出接近0或者1时,\sigma '(z)就会非常小,接近0,使得求得的梯度很小,损失函数收敛的很慢。
但是如果使用交叉熵的话就不会,它的导数就是一个差值,误差大的话更新的就快,误差小的话就更新的慢,因此,我们需要用交叉熵而不是MSE作为损失函数。
如果用数学证明很难理解,这里提供一张动图,可以帮助理解交叉熵(Cross Entropy)和方差(Square Error)的区别:绿线为交叉熵的平均loss曲线(凸函数),红线为平均方差loss曲线(非凸函数,有多个局部最优解),分别用来解决一个简单的二分类问题。

9、介绍一下SVM,遇到线性不可分怎么办,核函数有什么特点

从上面的介绍中,我们知道SVM显然是线性分类器,但数据如果根本就线性不可分怎么办?
数据在原始空间(称为输入空间)线性不可分,但是映射到高维空间(称为特征空间)后很可能就线性可分了
但是映射到高维空间同时带来一个问题:在高维空间上求解一个带约束的优化问题显然比在低维空间上计算量要大得多,这就是所谓的“维数灾难”。
于是就引入了“核函数”,核函数虽然也是把特征进行从低维到高维的转换,但是它事先在低维上进行计算,而将实质上的分类效果表现在了高维上,即避免了直接在高维空间中的复杂计算
常用的核函数包括:
  1. 线性核函数(Linear Kernel)表达式为:𝐾(𝑥,𝑧)=𝑥∙𝑧,就是普通的内积
  2. 多项式核函数(Polynomial Kernel)是线性不可分SVM常用的核函数之一,表达式为:K(x,z)=(γx∙z+r)^d,其中,各种参数都需要自己调参定义,调参比较麻烦
  3. 高斯核函数(Gaussian Kernel),在SVM中也称为径向基核函数(Radial Basis Function,RBF),它是libsvm默认的核函数,也是scikit-learn默认的核函数。表达式为:K(x,z)=exp(−γ||x−z||^2), 其中,γ大于0,需要自己调参定义
  4. Sigmoid核函数(Sigmoid Kernel)也是线性不可分SVM常用的核函数之一,表达式为:K(x,z)=tanh(γx∙z+r), 其中γ,r也都需要自己调参定义

代码示例

# 线性不可分的例子

# 样本生成
from sklearn.datasets import make_circles
X, y = make_circles(100, factor=.1, noise=.1)

clf = SVC(kernel='linear').fit(X, y)

plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf, plot_support=False)
# SVM
clf = SVC(kernel='rbf', C=1E6)
clf.fit(X, y)

plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf)
plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
            s=300, lw=1, facecolors='none')

延伸考点

本题考察 svm 核函数的特点,还会被问到常用的核函数(本文举了四个例子,建议掌握)

10、SVM的loss是啥

SVM 中经典的loss function 是 Hinge Loss,中文名也叫合页损失函数,因为它的图像就像一本打开的书,或者门和门框上的合页。

公式

\begin{gathered}<br />\sum_{i=1}^{N}\left[1-y_{i}\left(w \cdot x_{i}+b\right)\right]_{+}+\lambda\|w\|^{2} \\<br />其中{[z]_{+}=\left\{\begin{array}{l}<br />z, z>0 \\<br />0 . z \leq 0<br />\end{array}\right.}<br />\end{gathered}
第一项是损失,第二项是正则化项。这个公式就是说 $$y_{i}\left(w \cdot x_{i}+b\right)$$ 大于1时loss为0, 否则loss为$$1- y_{i}\left(w \cdot x_{i}+b\right)$$
  • Hinge Loss 不仅会惩罚错误的预测,还会惩罚不自信的正确预测。
  • Hinge Loss 简化了 SVM 的数***算,同时最大化了损失(与 Log-Loss 相比)。当我们想要做出实时决策而不过于关注准确性时使用它。
  • 正是因为Hinge Loss的零区域对应的正是非支持向量的普通样本,从而所有的普通样本都不参与最终超平面的决定,这才是支持向量机最大的优势所在,对训练样本数目的依赖大大减少,而且提高了训练效率。

Hinge loss 可视化

def hinge_loss(x):
    return np.maximum(0, 1 - x)

def plot_hinge_loss():
    x = np.arange(-5, 5, 0.1)
    y = hinge_loss(x)
    plt.plot(x, y, label="hinge loss")

    y2 = np.where(x>0, 0, 1)
    plt.plot(x, y2, label="0-1 loss")
    plt.legend()
    plt.title("nowcoder: offer+1")
    plt.show()


plot_hinge_loss()
  • 当x>=1 时,函数为0,
  • 当x<1 时,函数斜率为-1, 当t>1时,函数斜率为0.
另外,除了 hinge loss,还可以使用以下两种损失函数
1、指数损失(exponential loss):
y=exp(-x)
2、对率损失(logistic loss):
y=log(1+exp(-x))

延伸考点

损失函数里的正则项是干啥的?
提示:限制参数范围,避免过拟合(在深度学习中也常用该技巧)

11、Triplet Loss 怎么生成那三个点

原理

Triplet Loss是在谷歌的FaceNet论文中的提出来的,用于解决人脸识别相关的问题,原文为:《FaceNet: A Unified Embedding for Face Recognition and Clustering》,得到了广泛的应用。
Triplet 三元组指的是anchor, negative, positive 三个部分,每一部分都是一个 embedding 向量,其中
  • anchor指的是基准图片
  • positive指的是与anchor同一分类下的一张图片
  • negative指的是与anchor不同分类的一张图片
网络没经过学习之前,A和P的欧式距离可能很大,A和N的欧式距离可能很小,如上图左边,在网络的学习过程中,A和P的欧式距离会逐渐减小,而A和N的距离会逐渐拉大。
损失函数定义如下:
\mathcal{L}=\max (d(a, p)-d(a, n)+\operatorname{margin}, 0)
这时可以通过最小化上述损失函数,a与p之间的距离d(a,p)=0,而a与n之间的距离d(a,n)大于d(a,p)+margin。当negative example很好识别时,上述损失函数为0,否则是一个比较大的值。
也就是说,期望通过优化 triplet loss,使得类内紧凑,类间远离的目标。

上面提到,该 loss 是为了解决人脸识别相关问题,举个例子,我们的数据库中有 1000 个人,每个人有 20 张不同的图片,那么计算该 loss 需要计算(1000*20) * (999*20)次,后面的 999 表示negative example,即O(N^2)的复杂度。
所以 triplet loss 的三元组的选取也很关键。

三元组选取

基于triplet loss的定义,可以将triplet(三元组)分为三类:
  • easy triplets(简单三元组): triplet对应的损失为0的三元组,
  • hard triplets(困难三元组): negative example 与anchor距离小于anchor与positive example的距离,形式化定义为
  • semi-hard triplets(一般三元组): negative example 与anchor距离大于anchor与positive example的距离,但还不至于使得loss为0,即
在模型实际训练中,可以采用如下步骤选取以上三元组:
  1. 在每个mini-batch中,每个类别都有一定数量的正样本,和负样本
  2. 在mini-batch中挑选所有的anchor positive 图像对,同时,选择最为困难的anchor negative图像对
选择的原则是,选择同类图片中最不像的,作为 postive pair,选择不同类图片中最像的,作为negative pair。

代码实现(pytorch 版本)

Pytorch 官方已经支持了该函数:
torch.nn.TripletMarginLoss (margin=1.0, p=2.0, eps=1e-06, swap=False, size_average=None, reduce=None, reduction='mean')
from torch import nn

class TripletLoss(nn.Module):
    """Triplet loss with hard positive/negative mining.
    
    Args:
        margin (float, optional): margin for triplet. Default is 0.3.
    """
    
    def __init__(self, margin=0.3, global_feat, labels):
        super(TripletLoss, self).__init__()
        self.margin = margin
        self.ranking_loss = nn.MarginRankingLoss(margin=margin)
    def forward(self, inputs, targets):
        """
        Args:
            inputs (torch.Tensor): feature matrix with shape (batch_size, feat_dim).
            targets (torch.LongTensor): ground truth labels with shape (num_classes).
        """
        n = inputs.size(0)
        
        # 计算 embeddings 之间的距离
        dist = torch.pow(inputs, 2).sum(dim=1, keepdim=True).expand(n, n)
        dist = dist + dist.t()
        dist.addmm_(1, -2, inputs, inputs.t())
        dist = dist.clamp(min=1e-12).sqrt()  # for numerical stability
        
        # 关键,三元组的选取:为每个 sample找到hardest positive and negative
        mask = targets.expand(n, n).eq(targets.expand(n, n).t())
        dist_ap, dist_an = [], []
        for i in range(n):
            dist_ap.append(dist[i][mask[i]].max().unsqueeze(0))
            dist_an.append(dist[i][mask[i] == 0].min().unsqueeze(0))
        dist_ap = torch.cat(dist_ap)
        dist_an = torch.cat(dist_an)
        
        # 计算 loss
        y = torch.ones_like(dist_an)
        return self.ranking_loss(dist_an, dist_ap, y)

延伸考点

Triplet loss 的适用场景
Triplet loss通常是在个体级别的细粒度识别上使用,传统的分类是花鸟狗的大类别的识别,但是有些需求是要精确到个体级别,比如精确到哪个人的人脸识别,所以triplet loss的最主要应用也就是face identification,person re-identification,vehicle re-identification的各种identification识别问题上。
举个例子,中国有 14 亿人,如果做 softmax,开销非常大,不现实。这时就很适合用 triplet loss。

12、说说各种loss的书写

平方误差损失 L2 loss

每个训练示例的平方误差损失,也称为L2 损失,是实际值和预测值之差的平方:
相应的代价函数是这些平方误差均值(MSE)
MSE 损失函数。它是一个正二次函数(形式为 ax^2 + bx + c,其中 a > 0)二次函数只有全局最小值,因此,始终保证梯度下降将收敛(如果它完全收敛)到全局最小值。

绝对误差损失 L1 loss

每个训练示例的绝对误差是预测值和实际值之间的距离,与符号无关。绝对误差也称为L1 损失:
基于 L1的loss代价函数称为绝对误差平均值(MAE)

Smooth L1 Loss(Huber)

SmoothL1Loss为平方误差的修改版,为分段函数,对离散点不敏感,具体的公式如下:
\operatorname{smooth}_{L 1}(x)=\left\{\begin{array}{c}<br />0.5 x^{2},|x|<1 \\<br />|x|-0.5,|x| \geqslant 1<br />\end{array}\right.
这三种函数的可视化结果如下:

合页损失 Hinge loss

Hinge 损失主要与类标签为 -1 和 1 的 SVM分类器一起使用。
Hinge Loss 不仅会惩罚错误的预测,还会惩罚不自信的正确预测。
Hinge Loss 简化了 SVM 的数***算,同时最大化了损失(与 Log-Loss 相比)。当我们想要做出实时决策而不过于关注准确性时使用它。
输入-输出对 (x, y) 的Hinge loss为:

二分类交叉熵损失函数Binary Cross Entropy Loss

给出了离散版、连续版两个函数,主要区别是离散用求和,连续用积分。
S=\left\{\begin{aligned}<br />-& \int p(x) \cdot \log p(x) d x, & & \text { if } x \text { is continuous } \\<br />&-\sum_{x} p(x) \cdot \log p(x), & & \text { if } x \text { is discrete }<br />\end{aligned}\right.
负号用于使总量为正。概率分布的熵值越大,表示分布的不确定性越大。相反,熵越小表示模型预测的越确定。
二分类化简之后的结果如下:
L=-y * \log (p)-(1-y) * \log (1-p)=\left\{\begin{array}{ll}<br />-\log (1-p), & \text { if } y=0 \\<br />-\log (p), & \text { if } y=1<br />\end{array}\right.

多分类交叉熵损失函数Categorical Cross Entropy Loss

L\left(X_{i}, Y_{i}\right)=-\sum_{j=1}^{c} y_{i j} * \log \left(p_{i j}\right)<br />\\<br />where, \begin{aligned}<br />&y_{i j}=\left\{\begin{array}{l}<br />1, \quad \text { if } i_{\text {th }} \text { element is in class } j \\<br />0, & \text { otherwise }<br />\end{array}\right. \\<br />&p_{i j}=f\left(X_{i}\right)=\text { Probability that } i_{\text {th }} \text { element is in class } j<br />\end{aligned}

代码实现

from math import log
import matplotlib.pyplot as plt

import numpy as np


def L1_loss(predicted, actual):
    geterror = np.absolute(actual - predicted)
    totalloss = np.sum(geterror)
    return totalloss


def L2_loss(predicted, actual):
    geterror = np.absolute(actual - predicted)
    geterror = geterror ** 2
    totalloss = np.sum(geterror)
    return totalloss


def Mean_Squared_Error(predicted, actual):
    sum_square_error = 0.0
    for i in range(len(actual)):
        sum_square_error += (actual[i] - predicted[i]) ** 2.0
    mean_square_error = 1.0 / len(actual) * sum_square_error
    return mean_square_error
    
    
def smooth_l1_loss(predicted, actual, sigma, reduce=True, normalizer=1.0):
    beta = 1. / (sigma ** 2)
    diff = np.absolute(predicted - actual)
    cond = diff < beta
    loss = np.where(cond, 0.5 * diff ** 2 / beta, diff - 0.5 * beta)
    if reduce:
        return np.sum(loss) / normalizer
    return np.sum(loss, axis=1) / normalizer
    
    
def hinge_loss(predicted, actual):
    loss = np.maximum(0, 1 - predicted * actual)
    return loss
    
    
def Binary_Cross_Entropy(actual, predicted):
    sum_score = 0.0
    for i in range(len(actual)):
        sum_score += actual[i] * log(1e-15 + predicted[i])
    mean_sum_score = 1.0 / len(actual) * sum_score
    return -mean_sum_score



def Categorical_Cross_Entropy(actual, predicted):
    sum_score = 0.0
    for i in range(len(actual)):
        for j in range(len(actual[i])):
            sum_score += actual[i][j] * log(1e-15 + predicted[i][j])
    mean_sum_score = 1.0 / len(actual) * sum_score
    return -mean_sum_score

延伸考点

l1、l2、smooth l1 的区别?
提示,从函数的图像、计算复杂度、导函数考虑

13、请你介绍一下HMM

隐马尔科夫模型(Hidden Markov Model,HMM)是比较经典的机器学习模型,在语言识别,自然语言处理,模式识别等领域都有广泛的应用。但随着深度学习的崛起,尤其是RNNLSTM等神经网络序列模型的火热,HMM的地位有所下降。

适合使用HMM模型的问题一般有如下两个特征:
  1. 问题是基于序列的,比如时间序列,或者状态序列。
  2. 问题中有两类数据,一类序列数据是可以观测到的,即观测序列;而另一类数据是不能观察到的,即隐藏状态序列,简称状态序列。
比如打字的时候,打了前面,后面的自动给出提示(预测),就很适合该模型。
给出一个经典的示意图
  1. 初始化局部状态
  2. 利用动态规划递推时刻t=2,3,4,... T 的局部状态
  3. 计算最后时刻T的最大的状态 ,即为最可能隐藏状态序列出现的概率。计算时刻T最可能的隐藏状态
  4. 开始回溯T-1, T-2... 1的状态

延伸考点

HMM 是经典的机器学习模型,跟 SVM 类似,需要极其复杂的数学公式推导,除非你刻意提及“精通 HMM”,面试官一般不会刁难。
如果对此了解不多,则可以使用话术技巧,往自己熟悉的相关领域引导,比如文中提到的随着深度学习的崛起,尤其是RNNLSTM等神经网络序列模型的火热,HMM的地位有所下降。
然后就可以继续介绍 RNN、LSTM 这些主流的深度学习模型。

14、过拟合有哪些,你会从哪方面调节

过拟合的定义

由于模型过于复杂,模型学习能力过强,而用于训练的数据相对于复杂模型来说比较简单,因此模型学习到了数据中隐含的噪声,导致模型学习不到数据集的真正分布。
也就是说:模型在训练集上的准确率很高,但在测试集上的准确率却很低。

引起过拟合的原因

数据相对有限、简单,但模型结构过于复杂。
本质:算法从训练集的统计噪声中获取了信息并表达在模型结构的参数中。

解决办法(很多)

数据上

  • 加数据
  • 数据增强(mix up, fix up 等扩增方式)
理论上来讲:只要数据足够充足,就不会出现过拟合与欠拟合,但是显而易见,数据集的采集和制作成本很大。

模型上

  • 使用小模型
  • L1和L2正则化
  • Dropout
  • BatchNormalization (BN)
  • 使用残差结构(ResNet 中提出)
数据不好扩增的话,可以限制模型的表达能力,选择合适的网络结构,通过减少网络的深度、神经元数量、全连接层数等,降低网络的参数规模。
简化模型的另一个好处是能让模型更轻便、训练速度更快,计算速度也会更快。
还可以使用正则化、drop out 等技巧。

训练方式上

  • Early stopping
在神经网络的训练过程中,如果迭代次数过小,模型那么可能会欠拟合,但迭代次数过多,则会导致过拟合的发生。所以我们可以关注训练误差和测试误差的变化,在过拟合之前停止训练,下图可以帮助理解:

示例代码

pytorch 已经支持了很多函数,我们可以很方便地调用,比如 L2正则化
optimizer = torch.optim.SGD(model.parameters(),lr=0.01,weight_decay=0.001)
# weight_decay参数即调节 L2 正则化的超参数
但 L1 正则化需要自己手动实现
# L1 正则化
regularization_loss = 0
for param in model.parameters():
    regularization_loss += torch.sum(abs(param))

calssify_loss = criterion(pred,target)
loss = classify_loss + lamda * regularization_loss

optimizer.zero_grad()
loss.backward()
optimizer.step()
# dropout 函数
torch.nn.Dropout(p=0.5) 
# 以 0.5的概率随机关闭神经元

延伸考点

欠拟合呢?
提示:模型学习能力不够,换大模型,考虑更多特征

15、RF和GBDT谁更容易过拟合,偏差和方差

GDBT 更易过拟合。

首先解释偏差和方差

  • 偏差:度量了学习算法的期望预测与真实结果的偏离程度,即模型本身的精准度,反应出算法的拟合能力。
  • 方差:度量了同样大小的训练集的变动所导致的学习性能的变化,即模型的稳定性,反应出预测的波动情况。
《百面机器学习》中有类似下面的图,很直观地解释了高偏差和高方差的区别:
偏差和方差通常是矛盾的,降低偏差,会提高方差;降低方差,会提高偏差。

然后解释随机森林和 GDBT

GBDT 和 随机森林都是基于决策树而得到的。决策树比较容易理解,可以理解为一系列的 if-else,比较直观,可以筛选出最重要的特征,但是决策树比较容易过拟合,因此做了不少改进。

GBDT(Gradient Boosting Decison Tree)

GDBT 是经典的 boosting 方法,把所有树的结论累加起来做最终结论,其核心在于,每一棵树学的是之前所有树结论和的残差,即采用的是上一轮训练后得到的预测结果与真实结果之间的残差(残差是由损失函数计算得到的)。
GBDT在训练的过程中,随着树的个数的增加,训练偏差会一直减小,甚至减到0,这时候测试误差反而会很大,这个就是过拟合了。

Random Forest

随机森林则是基于bagging思想的经典模型,bagging 的核心就是投票,少数服从多少。
即利用bootstrap抽样,得到若干个数据集,每个数据集都训练一颗树。即每次只使用一部分数据集训练决策树,同时训练好多个,然后对这些模型的结果做投票。
对于随机森林,训练误差不会随着树的增加而一直减少,当树的数量足够多时,RF不会产生过拟合,提高树的数量能够使得错误率降低。

延伸考点

  • 如何解决高方差?
提示:降低模型复杂度、减少数据维度;降噪、增加样本数、使用验证集等;
  • 如何结果高偏差?
使用更合适的模型、增大数据量、数据预处理、清洗数据、过滤噪声等。

16、说说XGBoost和GBDT的不同

GBDT和XGBoost都属于集成学习(Ensemble Learning),集成学习的目的是通过结合多个基学习器的预测结果来改善单个学习器的泛化能力和鲁棒性。
GDBT 是Gradient Boosting的缩写,和其它 Boosting 算法一样,通过将表现一般的数个模型(通常是深度固定的决策树)组合在一起来集成一个表现较好的模型。可以说 Gradient Boosting = Gradient Descent + Boosting。
XGBoost的简称是XGB,由陈天奇(交大校友)提出的一种集成学习模型,属于boosting方法,可以通过减少偏差的方法,将若干弱学习器组合成强学习器,在各大竞赛中都展现了强大的威力。

两者的主要不同在于:
  1. 基分类器的选择:传统的GBDT以CART树作为基学习器,XGBoost还支持线性分类器,这个时候XGBoost相当于L1和L2正则化的逻辑斯蒂回归(分类)或者线性回归(回归);
  2. 梯度信息: 传统的GBDT在优化的时候只用到一阶导数信息,XGBoost则对代价函数进行了二阶泰勒展开,得到一阶和二阶导数;另外,xgboost工具支持自定义代价函数,只要函数可一阶和二阶求导。
  3. 正则项:XGBoost在代价函数中加入了正则项,用于控制模型的复杂度。从权衡方差偏差来看,它降低了模型的方差,使学习出来的模型更加简单,减轻过拟合,这也是XGBoost优于传统GBDT的一个特性;
  4. 学习率:shrinkage(缩减),相当于学习速率(XGBoost中的eta)。XGBoost在进行完一次迭代时,会将叶子节点的权值乘上该系数,主要是为了削弱每棵树的影响,让后面有更大的学习空间。(GBDT也有学习速率);
  5. 列抽样(column subsampling)。xgboost借鉴了随机森林的做法,支持列抽样,不仅能降低过拟合,还能减少计算,这也是xgboost异于传统gbdt的一个特性。
  6. 并行化:传统GBDT由于树之间的强依赖关系是无法实现并行处理的,但XGBoost工具支持并行。
  7. 除此之外,Xgboost实现了分裂点寻找近似算法、缺失值处理等包括一些工程上的优化,LightGBM是Xgboost的更高效实现。

延伸考点

集成学习有哪两种典型的思路?
提示:boosting 和 bagging。

17、说一说xgboost和lightgbm的区别是什么

前面提到了,LightGBM是Xgboost的更高效实现, 由微软发布。
XGBoost的并行是在特征粒度上的。我们知道,决策树的学习最耗时的一个步骤就是对特征的值进行排序(因为要确定最佳分割点),XGBoost在训练之前,预先对数据进行了排序,然后保存为block结构,后面的迭代中重复地使用这个结构,大大减小计算量。这个block结构也使得并行成为了可能,在进行节点的分裂时,需要计算每个特征的增益,最终选增益最大的那个特征去做分裂,那么各个特征的增益计算就可以开多线程进行。

LightGBM相比于Xgboost,添加了很多新的方法来改进模型,包括:并行方案、基于梯度的单边检测(GOSS)、排他性特征捆绑等。
LightGBM的设计思路主要是两点:1. 减小数据对内存的使用,保证单个机器在不牺牲速度的情况下,尽可能地用上更多的数据;2. 减小通信的代价,提升多机并行时的效率,实现在计算上的线性加速。由此可见,LightGBM的设计初衷就是提供一个快速高效、低内存占用、高准确度、支持并行和大规模数据处理的数据科学工具。
LightGBM并没有垂直的切分数据集,而是每个worker都有全量的训练数据,因此最优的特征分裂结果不需要传输到其他worker中,只需要将最优特征以及分裂点告诉其他worker,worker随后本地自己进行处理。处理过程如下:
  1. 每个worker在基于局部的特征集合找到最优分裂特征。
  2. workder间传输最优分裂信息,并得到全局最优分裂信息。
  3. 每个worker基于全局最优分裂信息,在本地进行数据分裂,生成决策树。
在AdaBoost中,采样权重作为数据实例重要性的衡量指标。然而在GBDT中,没有内含的样本权重,于是基于采样方法的权重不能应用于GBDT中。但如果实例梯度值小,误差就小,说明这个实例已经训练的很好了,直接的想法就是抛弃小梯度的数据,这样一来数据的分布就会发生改变,会损失学到的模型的精确度。
而LightGBM为了避免这个问题,提出了一种叫做GOSS的方法。GOSS保持有较大梯度的实例,在小梯度数据上运行随机采样。为了弥补这样做造成的数据分布的影响,当计算信息增益的时候,对于小梯度的数据,GOSS引入了常量乘法器。具体做法为,GOSS首先根据梯度绝对值排序,再选出a * 100%大梯度实例数据。之后在余下的数据随机采样b * 100%。经过这个过程,当计算信息增益时,GO***小梯度放大了采样数据1-a/b。这样做的好处在于,可以把更多的注意力放在训练实例上,而没有改变原始数据的分布。

总结起来,LightGBM相较于XGBoost的提升主要在于:
  1. 支持类别特征,无需one-hot编码;
  2. 采用叶节点分裂而非层分裂,学习精度更高;
  3. GOSS(基于梯度的单边采样),对海量学习数据,根据其梯度,筛除绝大部分的小梯度样本(几乎无更新作用),保持精度的同时加快速度;
  4. EFB(独立特征合并),针对海量稀疏数据,根据数据间的冲突度(如cos夹角,0101和1010的冲突很小,因为非零位不相同,非零位不相同的占比越高,冲突度越少),对冲突度小的特征进行合并,变稀疏矩阵为稠密矩阵,减少特征维度;
  5. 在直方图加速算法中,支持直方图减法,对下一层样本的直方图绘制,减少时间(右孩子直方图=父节点直方图-左孩子直方图);
  6. 优化了多线程加速问题。

XGBoost 使用示例

# XGBoost 使用示例
import xgboost as xgb
from sklearn import metrics
from sklearn.model_selection import GridSearchCV

def auc(m, train, test): 
    return (metrics.roc_auc_score(y_train,m.predict_proba(train)[:,1]),
           metrics.roc_auc_score(y_test,m.predict_proba(test)[:,1]))

# xgb 分类器
model = xgb.XGBClassifier()
param_dist = {"max_depth": [10,30,50],
              "min_child_weight" : [1,3,6],
              "n_estimators": [200],
              "learning_rate": [0.05, 0.1,0.16],}
# 网格法搜索超参
grid_search = GridSearchCV(model, param_grid=param_dist, cv = 3, 
                                   verbose=10, n_jobs=-1)
grid_search.fit(train, y_train)

grid_search.best_estimator_

model = xgb.XGBClassifier(max_depth=50, min_child_weight=1,  n_estimators=200,                    n_jobs=-1 , verbose=1,learning_rate=0.16)
model.fit(train,y_train)

auc(model, train, test)

Light GBM 使用示例

# Light GBM 使用示例
import lightgbm as lgb
from sklearn import metrics
from sklearn.model_selection import GridSearchCV

def auc2(m, train, test): 
    return (metrics.roc_auc_score(y_train,m.predict(train)),
                            metrics.roc_auc_score(y_test,m.predict(test)))

# 定义 lgb 分类器
lg = lgb.LGBMClassifier(silent=False)
param_dist = {"max_depth": [25,50, 75],
              "learning_rate" : [0.01,0.05,0.1],
              "num_leaves": [300,900,1200],
              "n_estimators": [200]
             }
             
# 网格法搜索超参
grid_search = GridSearchCV(lg, n_jobs=-1, param_grid=param_dist, cv = 3, scoring="roc_auc", verbose=5)
grid_search.fit(train,y_train)
grid_search.best_estimator_

d_train = lgb.Dataset(train, label=y_train)
params = {"max_depth": 50, "learning_rate" : 0.1, "num_leaves": 900,  "n_estimators": 300}

model2 = lgb.train(params, d_train)
auc2(model2, train, test) 

18、xgb的boosting如何体现,有什么特殊含义

XGBoost是一种集成学习方法。有时,仅仅依赖一个机器学习模型的结果可能是不够的。集成学习提供了一种系统的解决方案,结合多个学习者的预测能力。所得结果是一个单一模型,它给出了来自多个模型的聚合输出。

形成集成的模型,也称为基学习器(Base Learner),可以来自相同的学习算法或不同的学习算法。
在Boosting中,树是按顺序构建的,这样每个后续树的目的是减少前一棵树的错误。每个树从它的前辈学习并更新残余误差。因此,序列中生长的树将从残差的更新版本中学习。
Boosting的基学习器是偏向性高的弱学习器,其预测能力比随机猜测略好。这些弱学习器中的每一个都为预测结果贡献了一些重要的信息,使得boosting技术能够通过有效地结合这些弱学习器来产生强学习器。最终的强学习器会降低偏见和差异。
XGBoost模型是大规模并行boosting tree的工具,它是目前较好的开源boosting tree工具包。因此,在了解XGBoost算法基本原理之前,需要首先了解Boosting Tree算法基本原理。
Boosting Tree模型采用加法模型前向分步算法,而且基模型都是决策树模型。前向分步算法(Forward stage wise additive model)是指在叠加新的基模型的基础上同步进行优化,具体而言,就是每一次叠加的模型都去拟合上一次模型拟合后产生的残差(Residual)。
xgboost的目标函数如下:
O b j=\sum_{i=1}^{n} l\left(y_{i}, \hat{y}_{i}\right)+\sum_{k=1}^{K} \Omega\left(f_{k}\right)
其中第一项是训练误差,第二项是正则项,控制着模型的复杂度,包括了叶子节点数目T和leaf score的L2模的平方。

在Gradient Boost中,每个新模型的建立是为了使得之前模型的残差往梯度方向减少(当然也可以变向理解成每个新建的模型都给予上一模型的误差给了更多的关注),与传统Boosting对正确、错误的样本进行直接加权还是有区别的。

XGBoost算法的基本思想与GBDT类似,不断地地进行特征分裂来生长一棵树,每一轮学习一棵树,其实就是去拟合上一轮模型的预测值与实际值之间的残差。当我们训练完成得到k棵树时,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数,最后只需将每棵树对应的分数加起来就是该样本的预测值。

总结来说,Xgboost是Boosting算法的其中一种,Boosting算法的思想是将许多弱分类器集成在一起,形成一个强分类器。Xgboost是一种提升树模型,它是将许多树模型集成在一起,形成一个很强的分类器。而所用到的树模型则是CART回归树模型。 Xgboost是在GBDT的基础上进行改进,使之更强大,适用于更大范围。

延伸考点

  • xgb 的基学习器是?
    • CART
  • 还知道那些决策树?
    • ID3
    • C4.5
    • CART
    • 等等

19、讲讲数据倾斜怎么处理

概念:大数据场景下,MapReduce程序执行时,reduce节点大部分执行完毕,但是有一个或者几个reduce节点运行较慢,导致整个程序的处理时间很长,这是因为某一个key的条目数比其他key多很多(甚至数百倍),这条key所在的reduce节点所处理的数据量比其他节点大很多,从而导致某几个节点迟迟运行不完。Hive的执行是分阶段的,map处理数据量的差异取决于上一个stage的reduce输出,所以如何将数据均匀的分配到各个reduce中,就是解决数据倾斜的根本所在。
常见数据倾斜情形:
关键词 情形 后果
Join 其中一个表较小,但是key集中 分发到某一个或某几个reduce上的数据远高于平均值
大表与大表,但是分桶的判断字段0或空值过多 这些空值都由一个reduce处理,效率很慢
Group by Group by维度过小,某值的数量过多 处理某值的reduce非常耗时
Count distinct 某特殊值过多 处理此特殊值的reduce耗时
解决方案:
(1)增加jvm内存,适用于唯一值非常少,极少数值有非常多的记录值,该情况下只能通过硬件的手段来调优,增加jvm内存可以显著的提高运行效率。
(2)增加reduce的个数,适用于唯一值比较多,该字段某些值有远远多于其他值的记录数,该情况下最容易造成的就是大量相同的key被partition到一个分区,从而一个reduce执行了大量的工作,如果增加了reduce的个数,计算的节点就多了,有益处。
(3)重新设计key,在map阶段给key加上一个随机数,有了随机数的key就不会被大量的分配到同一节点,等到reduce后再把随机数去掉即可。
(4)使用combinner合并,combinner是在map阶段,reduce之前的一个中间阶段,在该阶段可以选择性的把大量的相同key数据先进行一个合并,然后再交给reduce来处理。减轻了map端向reduce端发送的数据量,也减轻了map端和reduce端中间的shuffle阶段的数据拉取数量。

20、xgb的分类树也是用残差吗,不是的话是什么

对于gbdt,训练过程每一轮拟合对象都是残差,但是xgb有所区别。
xgb采用的是回归树,预测值也为回归值,看起来与二分类问题有所冲突,解决方法是将二分类问题作为逻辑回归问题来处理,定义logloss = ylog(p)+(1-y)log(1-p) ,p = 1/(1+exp(-x))。这样代入xgb的优化式子中,x即为最终的各样本叶子值的和,经过sigmoid函数之后得到概率值,也就解决了回归值与分类之间的问题。
xgb本质还是希望损失函数逐级下降,只不过其直接将每一轮的 损失函数通过泰勒展开之后,直接求出决策树的最优叶子值,也就是说,对于给定的一棵树,能够找到这棵树对应的最优叶子值,但是直接寻找这棵树是不可能的,要采取贪心的算法逐级分裂,事实上这个分裂过程和原本的决策树(ID3, CART)等几乎没有区别,区别在于分裂原则不同,xgb的分裂原则是自己的一套Gain,推导过程: Obj = \sum\limits_{i=1}^nl(y_{i}, \hat{y}_{i} )+\sum\limits_{k=1}^KΩ(f_{k} )
其中加号左侧项经泰勒展开后为:l(y_{i}, \hat{y}_i^{(t-1)} )+g_{i}f_{t}(x_{i})+0.5h_{i}f_{t}^2(x_{i})
在这里包含一棵树的定义: f_{t}(x)=w_{q}(x),w∈R^T, q:R^d->{1, 2, ..., T}
而加号右侧前t-1棵树常量化得到: Ω(f_{t})+constant
该部分定义了一棵树的复杂度: Ω(f_{t})=λT+0.5λ \sum\limits_{j=1}^{T}w_{j}^2
叶子节点分组: \sum\limits_{j=1}^T[( \sum\limits_i∈I_{j}, g_{i} )w_{j}+0.5(\sum\limits_i∈I_{j}h_{i}+λ)w_{j}^2]+λT
最终目标函数: \sum\limits_{j=1}^T [G_{j}w_{j}+0.5(H_{j}+λ)w_{j}^2]+λT
最优点:w_{j}^* = - \frac{G_{j}}{H_{j}+λ}, Obj = -0.5 \sum\limits_{j=1}^T \frac{G_{j}^2}{H_{j}+λ}+λT
叶子结点分类依据 : Gain = 0.5[ \frac{G_{L}^2}{H_{L}+λ} +\frac{G_{R}^2}{H_{R}+λ}-\frac{(G_{L}+G_{R})^2}{H_{L}+H_{R}+λ}]-λ
Gain是根据obj的差值计算出来的,因此这里不需要给定拟合值y,只需要一阶、二阶梯度就可以计算Gain。

21、请你说说TFIDF的公式
词频-逆文本频率,从英文上很好理解:
TF-IDF = Term Frequency (TF) * Inverse Document Frequency (IDF)
其中:
词频t f_{i, j}=\frac{n_{i, j}}{\sum_{k} n_{i, j}}
逆文本频率i d f(w)=\log \left(\frac{N}{d f_{t}}\right)
TF-IDF: w_{i, j}=t f_{i, j} \times \log \left(\frac{N}{d f_{i}}\right)

代码实现

# 词频
def computeTF(wordDict, bagOfWords):
    tfDict = {}
    bagOfWordsCount = len(bagOfWords)
    for word, count in wordDict.items():
        tfDict[word] = count / float(bagOfWordsCount)
    return tfDict

# 逆文本频率
def computeIDF(documents):
    import math
    N = len(documents)
    
    idfDict = dict.fromkeys(documents[0].keys(), 0)
    for document in documents:
        for word, val in document.items():
            if val > 0:
                idfDict[word] += 1
    
    for word, val in idfDict.items():
        idfDict[word] = math.log(N / float(val))
    return idfDict
   
# TF-IDF
def computeTFIDF(tfBagOfWords, idfs):
    tfidf = {}
    for word, val in tfBagOfWords.items():
        tfidf[word] = val * idfs[word]
    return tfidf 

22、请你说说tfidf

TF-IDF(Term Frequency & Inverse Documentation Frequency 词频-逆文本频率)算法是非常常用、非常基础的一种文本特征的提取方法,在文本信息检索,语意抽取等自然语言处理(NLP)中广泛应用。也是谷歌这家公司发迹的核心算法之一。

tfi-df 的理论

TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。核心思想就两条:
  1. 字词的重要性随着它在文件中出现的次数成正比增加
  2. 但同时会随着它在语料库中出现的频率成反比下降
tf-idf加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。
比如我们想要在百度搜索一个词条,互联网有海量的信息,怎么找出最相关的信息呢?
首先,第一条很容易理解,我要找的页面里肯定要包含该词条,而且出现多次的应该比只出现 1 次的更相关;
但如果该词条是很常用的词,比如停用词(你,我,他、的、地、得等等),这些词本身并不包含很多信息,基本每个文档里面都有,那么这些词就没那么重要。反而那些包含信息量多的词,比如“核爆炸”、“牛客网”等等,相比于停用词就更关键:只有特定的页面才会包含该词条。
一句话总结,就是一个词语在一篇文章中出现次数越多, 同时在所有文档中出现次数越少, 越能够代表该文章。

tf-idf 的不足

本质上idf是一种试图抑制噪声的加权,并且单纯地认为文本频率小的单词就越重要,文本频率大的单词就越无用,显然这并不是完全正确的。
idf的简单结构并不能有效地反映单词的重要程度和特征词的分布情况,使其无法很好地完成对权值调整的功能。
此外,在tf-idf算法中并没有体现出单词的位置信息,对于Web文档而言,权重的计算方法应该体现出HTML的结构特征。特征词在不同的标记符中对文章内容的反映程度不同,其权重的计算方法也应不同。因此应该对于处于网页不同位置的特征词分别赋予不同的系数,然后乘以特征词的词频,以提高文本表示的效果。(比如很多 seo 都会建很多垃圾网站,在 html 里添加各种热搜关键词,但并不显示在前端,以规避审查)

延伸考点

tf-idf 的不足?
提示:参见上文回答

23、请你说说决策树

概念

决策树是一种有监督的机器学习算法,该方法可以用于解决分类和回归问题。
决策树可以简单地理解为达到某一特定结果的一系列决策。思考逻辑上,就像一连串的if-else,如果满足 xx 特征,则归为 xx 类别,否则则归为 yy 类别。(可以参考周志华老师《机器学习》里挑西瓜的案例)
这其中的关键,就是如何选取特征。一棵树能选取的特征往往有限,限制了模型的性能。因此就有了随机森林。

特征选择

特征选择决定了使用哪些特征来做判断。在训练数据集中,每个样本的属性可能有很多个,不同属性的作用有大有小。因而特征选择的作用就是筛选出跟分类结果相关性较高的特征,也就是分类能力较强的特征。
在特征选择中通常使用的准则是:熵H(D)=-\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|} \log _{2} \frac{\left|C_{k}\right|}{|D|}其中 c_k 表示集合 D 中属于第 k 类样本的样本子集。

ID3 算法

ID3 是最早提出的决策树算法,他就是利用信息增益来选择特征的,它表示得知特征 A 的信息而使得样本集合不确定性减少的程度。
条件熵\begin{aligned} H(D \mid A) &=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} H\left(D_{i}\right) \\ &=-\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|}\left(\sum_{k=1}^{K} \frac{\left|D_{i k}\right|}{\left|D_{i}\right|} \log _{2} \frac{\left|D_{i k}\right|}{\left|D_{i}\right|}\right) \end{aligned}
信息增益=信息熵-条件熵 \operatorname{Gain}(D, A)=H(D)-H(D \mid A)

C4.5 算法

是 ID3 的改进版,不是直接使用信息增益,而是引入“信息增益比”指标作为特征的选择依据。
\begin{aligned} \operatorname{Gain}_{\text {ratio }}(D, A) &=\frac{\operatorname{Gain}(D, A)}{H_{A}(D)} \\ H_{A}(D) &=-\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} \log _{2} \frac{\left|D_{i}\right|}{|D|} \end{aligned}

CART(Classification and Regression Tree)

这种算法即可以用于分类,也可以用于回归问题。CART 算法使用了基尼系数取代了信息熵模型。
\begin{aligned} \operatorname{Gini}(D) &=\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|}\left(1-\frac{\left|C_{k}\right|}{|D|}\right) \\ &=1-\sum_{k=1}^{K}\left(\frac{\left|C_{k}\right|}{|D|}\right)^{2} \\ \operatorname{Gini}(D \mid A) &=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} \operatorname{Gini}\left(D_{i}\right) \end{aligned}

优缺点

优点
  • 决策树易于理解和解释,可以可视化分析,容易提取出规则;
  • 可以同时处理标称型和数值型数据;
  • 比较适合处理有缺失属性的样本;
  • 能够处理不相关的特征;
  • 测试数据集时,运行速度比较快;
  • 在相对短的时间内能够对大型数据源做出可行且效果良好的结果。
缺点
  • 容易发生过拟合(随机森林可以很大程度上减少过拟合);
  • 容易忽略数据集中属性的相互关联;
  • 对于那些各类别样本数量不一致的数据,在决策树中,进行属性划分时,不同的判定准则会带来不同的属性选择倾向。

代码实现

# sklearn 提供了各种经典决策树的接口
>>> from sklearn import tree
>>> X = [[0, 0], [1, 1]]
>>> Y = [0, 1]
>>> clf = tree.DecisionTreeClassifier()
>>> clf = clf.fit(X, Y)1

# 模型可以用来预测样本的类别:
>>> clf.predict([[2., 2.]])
array([1])
# 预测每个类的概率,在叶片上同一类的训练样本的分数
>>> clf.predict_proba([[2., 2.]])
array([[ 0.,  1.]])

延伸考点

各个经典的决策树算法有啥区别?
提示:信息增益准则对可取数目较多的属性有所偏好(典型代表ID3算法),而增益率准则(CART)则对可取数目较少的属性有所偏好,但CART进行属性划分时候不再简单地直接利用增益率尽心划分,而是采用一种启发式规则)(只要是使用了信息增益,都有这个缺点,如RF)

24、谈一谈决策树的实现逻辑 信息增益、信息增益率公式

决策树的实现逻辑

决策树是最简单的机器学习算法,它易于实现,可解释性强,完全符合人类的直观思维,有着广泛的应用。
决策树算法采用树形结构,使用层层推理来实现最终的分类。决策树由下面几种元素构成:
  • 根节点:包含样本的全集
  • 内部节点:对应特征属性测试
  • 叶节点:代表决策的结果
预测时,在树的内部节点处用某一属性值进行判断,根据判断结果决定进入哪个分支节点,直到到达叶节点处,得到分类结果。这是一种基于 if-then-else 规则的有监督学习算法,决策树的这些规则通过训练得到,而不是人工制定的。

信息增益的公式

信息增益=信息熵-条件熵
其中信息熵:H(D)=-\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|} \log _{2} \frac{\left|C_{k}\right|}{|D|}其中 c_k 表示集合 D 中属于第 k 类样本的样本子集。
条件熵:\begin{aligned} H(D \mid A) &=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} H\left(D_{i}\right) \\ &=-\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|}\left(\sum_{k=1}^{K} \frac{\left|D_{i k}\right|}{\left|D_{i}\right|} \log _{2} \frac{\left|D_{i k}\right|}{\left|D_{i}\right|}\right) \end{aligned}
信息增益:\operatorname{Gain}(D, A)=H(D)-H(D \mid A)

信息增益率的公式

信息增益率:\begin{aligned} \operatorname{Gini}(D) &=\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|}\left(1-\frac{\left|C_{k}\right|}{|D|}\right) \\ &=1-\sum_{k=1}^{K}\left(\frac{\left|C_{k}\right|}{|D|}\right)^{2} \\ \operatorname{Gini}(D \mid A) &=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} \operatorname{Gini}\left(D_{i}\right) \end{aligned}

25、请你说说逻辑回归
Logistic 回归,即逻辑回归也是从统计学中借鉴来的,尽管名字里有回归俩字儿,但它不是一个需要预测连续结果的回归算法。
与之相反,Logistic 回归是二分类任务的首选方法。它输出一个 0 到 1 之间的离散二值结果。简单来说,它的结果不是 1 就是 0。
Logistic 回归通过使用其固有的 logistic 函数估计概率,来衡量因变量(我们想要预测的标签)与一个或多个自变量(特征)之间的关系。
然后这些概率必须二值化才能真地进行预测。这就是 logistic 函数的任务,也称为 Sigmoid 函数。Sigmoid 函数是一个 S 形曲线,输出范围是 0 到 1,所有可以理解为它对每个神经元的输出进行了归一化,也即有了概率的意义。(很适合分类模型)然后使用阈值分类器将 0 和 1 之间的值转换为 0 或 1。

logistics函数可视化代码

def sigmoid(x):
    return 1. / (1. + np.exp(-x))
def plot_sigmoid():
    x = np.arange(-10, 10, 0.1)
    y = sigmoid(x)
    plt.plot(x, y, label="sigmoid")
    plt.legend()
    plt.title("nowcoder: offer+1")
    plt.show()

plot_sigmoid()

优缺点

优点:
  • 速度快,适合二分类问题
  • 简单易于理解,直接看到各个特征的权重
  • 能容易地更新模型吸收新的数据
缺点:
  • 对数据和场景的适应能力有局限性,不如决策树算法适应性那么强

代码示例

from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import numpy as np

def logisticRegression():
    data = ...
    X = data[:,0:-1]
    y = data[:,-1]

    # 划分为训练集和测试集
    x_train,x_test,y_train,y_test = train_test_split(X,y,test_size=0.2)

    # 归一化
    scaler = StandardScaler()
    # scaler.fit(x_train)
    x_train = scaler.fit_transform(x_train)
    x_test = scaler.fit_transform(x_test)

    # 逻辑回归
    model = LogisticRegression()
    model.fit(x_train,y_train)

    # 预测
    predict = model.predict(x_test)
    right = sum(predict == y_test)
    
    # 将预测值和真实值放在一块,好观察
    predict = np.hstack((predict.reshape(-1,1),y_test.reshape(-1,1)))   
    print(predict)

延伸考点

为什么不用平方误差?
提示,从loss 反向传播的梯度考虑,具体可以参见<为啥LR不使用MSE而使用交叉熵损失函数的原因>

26、请你说说回归问题可以设置支持向量机吗

支持向量分类方法可以推广到解决回归问题。这种方法称为支持向量回归,即support vector regression(SVR)
svr的优化目标是l2 regularization+ c*epsilon-sensitive error. 前者正则化是为了控制模型复杂度不必多说,后者epsilon-sensitive error是理解svr的关键,但其实对照linear regression的损失函数也很容易,就如下图
传统的回归方法当且仅当回归f(x)完全等于y时才认为是预测正确,需计算其损失;
而支持向量回归(SVR)则认为只要是f(x)与y偏离程度不要太大,既可认为预测正确,不用计算损失。
具体的就是设置一个阈值α,只是计算 |f(x) - y| > α 的数据点的loss。如图,支持向量回归表示只要在虚线内部的值都可认为是预测正确,只要计算虚线外部的值的损失即可。

代码示例

# 使用方法与与分类一样
# fit方法将把向量X,y作为参数向量,
# 只是在这种情况下,只在 y 是浮点数而不是整数型
>>> from sklearn import svm
>>> X = [[0, 0], [2, 2]]
>>> y = [0.5, 2.5]
>>> regr = svm.SVR()
>>> regr.fit(X, y)
SVR()
>>> regr.predict([[1, 1]])
array([1.5])


全部评论

相关推荐

头像
11-09 12:17
清华大学 C++
out11Man:小丑罢了,不用理会
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-21 19:05
面试官_我太想进步了:混学生会的,难怪简历这么水
点赞 评论 收藏
分享
点赞 4 评论
分享
牛客网
牛客企业服务