Adam 优化器
结合 AdaGrad 和 RMSProp 两种优化算法的优点。对梯度的一阶矩估计(First Moment Estimation,即梯度的均值)和二阶矩估计(Second Moment Estimation,即梯度的未中心化的方差)进行综合考虑,计算出更新步长。
二阶矩: 梯度平方累积和的平方根 (时间上)。此项能够累积各个参数 gt,i 的历史梯度平方,频繁更新的梯度,则累积的分母项逐渐偏大,那么更新的步长(stepsize)相对就会变小,而稀疏的梯度,则导致累积的分母项中对应值比较小,那么更新的步长则相对比较大。
1x1卷积( Network in Network)的作用
由于使用了最小窗口,1×1卷积失去了卷积层可以识别高和宽维度上相邻元素构成模式的功能。假设将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么1×1卷积层的作用与全连接层等价。
梯度消失解决方法?
梯度爆炸解决方法?
batch size对收敛速度,模型训练的影响?其大小是怎么选取的?
凸函数与凸优化:
凸函数:
凸优化:研究凸函数最小化的问题,凸优化中局部最优值就是全局最优值。
凸优化问题的例子包括线性回归、逻辑回归、支持向量机等线性模型,非凸优化问题的例子包括低秩模型(如矩阵分解)、深度神经网络模型等。
奥卡姆剃刀:“如无必要,勿增实体”,即“简单有效原理”。如果有一个简单的方法和一个复杂的方法能够达到同样的效果,我们应该选择简单的那个,因为简单的选择是巧合的几率更小,更有可能反应事物的内在规律。
训练一个很大的深度神经网络可能很慢,如何加速训练?
主要有以下五种方式:连接权重使用好的初始化策略;使用复杂度较低的激活函数;批量归一化;使用预训练网络;使用快速优化器代替常规的梯度下降。
什么是生成模型和判别模型?
生成模型:由数据学习联合概率分布P(X,Y),然后除以P(X)得到条件概率分布P(Y|X),作为预测的模型。之所以被称为生成方法,是因为模型表示了给定输入X产生输出Y的关系。常见的生成模型有:朴素贝叶斯、隐马尔可夫模型、高斯混合模型、文档主题生成模型(LDA)、限制玻尔兹曼机。
判别模型:判别模型由数据直接学习决策函数f(X),或者条件概率分布P(Y|X)作为预测模型。判别方法关心的是对给定的输入X,应该预测什么样的输出Y。常见的判别模型有:K近邻、感知机、线性回归、线性判别分析(LDA)、LR、SVM、决策树、神经网络、boosting、条件随机场、最大熵模型。
在监督学习中,生成方法和判别方法各有优缺点,适合于不同条件下的学习问题。
生成方法的特点:生成方法可以还原出联合概率分布P(X,Y),而判别方法则不能;生成方法的学习收敛速度更快,即当样本容量增加的时候,学到的模型可以更快地收敛于真实模型;当存在隐变量时,仍可以用生成方法学习,此时判别方法就不能用。
判别方法的特点:判别方法直接学习的是条件概率P(Y|X)或决策函数f(X),直接面对预测,往往学习的准确率更高;由于直接学习P(Y|X)或f(X),可以对数据进行各种程度上的抽象、定义特征并使用特征,因此可以简化学习问题。
由生成模型可以得到判别模型,但由判别模型得不到生成模型。
如果训练的神经网络不收敛,可能有哪些原因?如何加快收敛速度。
深度学习超参调节的一些技巧
python生成器,迭代器,装饰器
27、感受野
定义:感受野用来表示网络内部的不同神经元对原图像的感受范围的大小,或者说,convNets(cnn)每一层输出的特征图(feature map)上的像素点在原始图像上映射的区域大小。
神经元之所以无法对原始图像的所有信息进行感知,是因为在这些网络结构中普遍使用卷积层和 pooling 层,在层与层之间均为局部连接。
神经元感受野的值越大表示其能接触到的原始图像范围就越大,也意味着它可能蕴含更为全局,语义层次更高的特征;相反,值越小则表示其所包含的特征越趋向局部和细节。因此感受野的值可以用来大致判断每一层的抽象层次。
池化的作用
减少参数。通过对 Feature Map 降维,有效减少后续层需要的参数
Translation Invariance。它表示对于 Input,当其中像素在邻域发生微小位移时,Pooling Layer 的输出是不变的。这就使网络的鲁棒性增强了,有一定抗扰动的作用(平移不变性)
特征提取的误差主要来自两个方面:
(1)邻域大小受限造成的估计值方差增大;
(2)卷积层参数误差造成估计均值的偏移。
一般来说,mean-pooling 能减小第一种误差,更多的保留图像的背景信息,max-pooling 能减小第二种误差,更多的保留纹理信息。
inception 的作用
使用 1x1 的卷积来进行升降维;在多个尺寸上同时进行卷积再聚合。
Googlenet 的特点
1、inception
2、多个 loss: Inception Net 有 22 层深,除了最后一层的输出,还使用到了辅助分类节点,即将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中,相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化。
VGG 的特点
卷积层使用更小的 filter 尺寸和间隔
3×3 卷积核的优点:
多个 3×3 的卷基层比一个大尺寸 filter 卷基层有更多的非线性及更少的参数,假设卷基层的输入和输出的特征图大小相同为 C,那么三个 3×3 的卷积层参数个数 3×(3×3×C)=27C;一个 7×7的卷积层参数为 49C;所以可以把三个 3×3 的 filter 看成是一个 7×7filter 的分解(中间层有非线性的分解)
卷积核大小必须大于 1 才有提升感受野的作用,1 排除了。而大小为偶数的卷积核即使对称地加 padding 也不能保证输入 feature map 尺寸和输出 feature map 尺寸不变,2 排除了。所以一般都用 3 作为卷积核大小。
1*1 卷积核的优点:
作用是在不影响输入输出维数的情况下,对输入进行线性形变,然后通过 Relu 进行非线性处理,增加网络的非线性表达能力。
特征选择:
从所有特征中选取最小特征子集是一个NP难问题,即除了穷举式搜索,不能保证找到最优解。一般使用前向搜索和后向搜索的策略,使用贪心算法寻找次优解。特征选择主要有过滤法,封装法,嵌入法:
过滤法(Filter):
卡方检验是检验类别型自变量对类别型因变量的相关性。假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距,构建统计量:
该统计量的含义为自变量对因变量的相关性,
互信息也是评价定性自变量对定性因变量的相关性的,互信息计算公式如下:
包装法(Wrapper):
嵌入法(Embedded):
使用某些机器学习算法进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。主要有基于惩罚项和基于树模型的特征选择法
使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维。 L1惩罚项降维的原理在于保留多个对目标值具有同等相关性的特征中的一个,所以没选到的特征不代表不重要。故,可结合L2惩罚项来优化。具体操作为:若一个特征在L1中的权值为1,选择在L2中权值差别不大且在L1中权值为0的特征构成同类集合,将这一集合中的特征平分L1中的权值,故需要构建一个新的逻辑回归模型:
树模型中GBDT也可用来作为基模型进行特征选择
from sklearn.feature_selection import VarianceThreshold
# 方差选择法,返回值为特征选择后的数据
var_filter = VarianceThreshold(threshold=3)
var_filter.fit_transform(iris.data) # 参数threshold为方差的阈值
from sklearn.feature_selection import SelectKBest
from scipy.stats import pearsonr
# 选择K个最好的特征,返回选择特征后的数据
# 第一个参数为计算评估特征是否好的函数,该函数输入特征矩阵和目标向量,输出二元组(评分,P值)的数组,数组第i项为第i个特征的评分和P值。在此定义为计算相关系数
# 参数k为选择的特征个数
corr_filter = SelectKBest(lambda X, Y: array(map(lambda x:pearsonr(x, Y), X.T)).T, k=2)
corr_filter.fit_transform(iris.data, iris.target)
from sklearn.feature_selection import chi2
# 选择K个最好的特征,返回选择特征后的数据
SelectKBest(chi2, k=2).fit_transform(iris.data, iris.target)
from sklearn.feature_selection import SelectKBest
from minepy import MINE
# 由于MINE的设计不是函数式的,定义mic方法将其为函数式的,返回一个二元组,二元组的第2项设置成固定的P值0.5
def mic(x, y):
m = MINE()
m.compute_score(x, y)
return (m.mic(), 0.5)
# 选择K个最好的特征,返回特征选择后的数据
SelectKBest(lambda X, Y: array(map(lambda x:mic(x, Y), X.T)).T, k=2).fit_transform(iris.data, iris.target)
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
# 递归特征消除法,返回特征选择后的数据
# 参数estimator为基模型
# 参数n_features_to_select为选择的特征个数
RFE(estimator=LogisticRegression(), n_features_to_select=2).fit_transform(iris.data, iris.target)
from sklearn.feature_selection import SelectFromModel
# 带L1惩罚项的逻辑回归作为基模型的特征选择
SelectFromModel(LogisticRegression(penalty="l1", C=0.1)).fit_transform(iris.data, iris.target)
# 带L1和L2惩罚项的逻辑回归作为基模型的特征选择
# 参数threshold为权值系数之差的阈值
SelectFromModel(LR(threshold=0.5, C=0.1)).fit_transform(iris.data, iris.target)
from sklearn.ensemble import GradientBoostingClassifier
# GBDT作为基模型的特征选择
SelectFromModel(GradientBoostingClassifier()).fit_transform(iris.data, iris.target)
回顾
类 | 所属方式 | 说明 |
VarianceThreshold | Filter | 方差选择法 |
SelectKBest | Filter | 可选关联系数、卡方校验、最大信息系数作为得分计算的方法 |
RFE | Wrapper | 递归地训练基模型,将权值系数较小的特征从特征集合中消除 |
SelectFromModel | Embedded | 训练基模型,选择权值系数较高的特征 |
不平衡数据处理问题(从数据层面和算法层面来回答)
数据之间如果不是独立同分布的会怎样
78、牛顿法和梯度下降对比
1. 牛顿法起始点不能离局部极小点太远,否则很可能不会收敛。(考虑到二阶拟合应该很容易想象),所以实际操作中会先使用别的方法,比如梯度下降法,使更新的点离最优点比较近,再开始用牛顿法。
2. 牛顿法每次需要更新一个二阶矩阵,当维数增加的时候是非常耗内存的,所以实际使用是会用拟牛顿法。
3. 梯度下降法在非常靠近最优点时会有震荡,就是说明明离的很近了,却很难到达,因为线性的逼近非常容易一个方向过去就过了最优点(因为只能是负梯度方向)。但牛顿法因为是二次收敛就很容易到达了。
牛顿法最明显快的特点是对于二阶函数(考虑多元函数的话要在凸函数的情况下),牛顿法能够一步到达,非常有效。
为什么多采用正态分布?
1、我们想要建模的很多分布的真实情况是比较接近正态分布的。中心极限定理说明很多独立随机变量的和近似服从正态分布。这意味着在实际中,很多复杂系统都可以被成功地建模成正态分布的噪声,即使系统可以被分解成一些更结构化的部分。
2、在具有相同方差的所有可能的概率分布中,正态分布在实数上具有最大的不确定性。因此,我们可以认为正态分布式对模型加入的先验知识量最少的分布。
DNN权重为什么要做初始化?要注意哪些问题?方法有哪些?(权重随机初始化,避免网络退化,初始化范围要小,缩小样本空间,输入样本要BN,防止梯度消失) kaiming初始化方法的过程是怎样的?
权重初始化的目的是防止在深度神经网络的正向(前向)传播过程中层激活函数的输出损失梯度出现爆炸或消失。如果发生任何一种情况,损失梯度太大或太小,就无法有效地向后传播,并且即便可以向后传播,网络也需要花更长时间来达到收敛。
在深度学习中,神经网络的权重初始化方法对(weight initialization)对模型的收敛速度和性能有着至关重要的影响。说白了,神经网络其实就是对权重参数w的不停迭代更新,以期达到较好的性能。在深度神经网络中,随着层数的增多,我们在梯度下降的过程中,极易出现梯度消失或者梯度爆炸。因此,对权重w的初始化则显得至关重要,一个好的权重初始化虽然不能完全解决梯度消失和梯度爆炸的问题,但是对于处理这两个问题是有很大的帮助的,并且十分有利于模型性能和收敛速度。
注意:不能使用全零初始化,因为如果网络中的每个神经元都计算出同样的输出,然后它们就会在反向传播中计算出同样的梯度,从而进行同样的参数更新。即:如果权重被初始化为同样的值,神经元之间就失去了不对称性的源头。
权重一定不能全零初始化。因为这会导致神经元在前向传播中计算出同样的输出,然后在反向传播中计算出同样的梯度,从而进行同样的权重更新。这就产生了大量对称性神经元。
初始化方法:
通常采用小随机数初始化,通过这样来打破对称性。至于使用高斯分布还是均匀分布,对结果影响很小。
之所以用小的随机数,是因为:如果网络中存在tanh
或者 sigmoid
激活函数,或者网络的输出层为sigmoid
等单元,则它们的变量值必须很小。
如果使用较大的随机数,则很有可能这些单元会饱和,使得梯度趋近于零。这意味着基于梯度下降的算法推进的很慢,从而学习很慢。
如果网络中不存在sigmoid/tanh
等激活函数,网络的输出层也不是sigmoid
等单元,则可以使用较大的随机数初始化。
回顾“多层感知机”一节图3.3描述的多层感知机。为了方便解释,假设输出层只保留一个输出单元o1o1(删去o2o2和o3o3以及指向它们的箭头),且隐藏层使用相同的激活函数。如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。
通常使用来校准权重初始化标准差。随着输入数据的增长,随机初始化的神经元的输出数据的分布中的方差也在增大。
还有一种比较常用的随机初始化方法叫作Xavier随机初始化 [1]。 假设某全连接层的输入个数为aa,输出个数为bb,Xavier随机初始化将使该层中权重参数的每个元素都随机采样于均匀分布。它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数影响,且每层梯度的方差也不该受该层输出个数影响。
神经网络的训练过程中的参数学习是基于梯度下降法进行优化的。梯度下 降法需要在开始训练时给每一个参数赋一个初始值。这个初始值的选取十分关 键。在感知器和logistic回归的训练中,我们一般将参数全部初始化为0。但是这 在神经网络的训练中会存在一些问题。因为如果参数都为0,在第一遍前向计算时,所有的隐层神经元的激活值都相同。这样会导致深层神经元没有区分性。这 种现象也称为对称权重现象。
为了打破这个平衡,比较好的方式是对每个参数都随机初始化,这样使得不 同神经元之间的区分性更好。
随机初始化参数的一个问题是如何选取随机初始化的区间。如果参数太小, 一是会导致神经元的输入过小,经过多层之后信号就慢慢消失了;二是还会使得 Sigmoid型激活函数丢失非线性的能力。以Logistic函数为例,在0附近基本上是 近似线性的。这样多层神经网络的优势也就不存在了。如果参数取得太大,会导 致输入状态过大。对于 Sigmoid 型激活函数来说,激活值变得饱和,从而导致梯 度接近于0。
因此,要高效地训练神经网络,给参数选取一个合适的随机初始化区间是非 常重要的。一般而言,参数初始化的区间应该根据神经元的性质进行差异化的 设置。如果一个神经元的输入连接很多,它的每个输入连接上的权重就应该小一 些,以避免神经元的输出过大(当激活函数为ReLU时)或过饱和(当激活函数为 Sigmoid函数时)。
经常使用的初始化方法有以下两种:
高斯分布初始化 参数从一个固定均值(比如0)和固定方差(比如0.01)的高斯 分布进行随机初始化。
均匀分布初始化 在一个给定的区间[−r, r]内采用均匀分布来初始化参数。超参 数r 的设置可以按神经元的连接数量进行自适应的调整。 初始化一个深层网络时,一个比较好的初始化策略是保持每个神经元输入 和输出的方差一致。这里介绍两种参数初始化的方法。
更详细的看深度学习与神经网络
12、解释最大似然估计和最小二乘法
最大似然估计:现在已经拿到了很多个样本(你的数据集中所有因变量),这些样本值已经实现,最大似然估计就是去找到那个(组)参数估计值,使得前面已经实现的样本值发生概率最大。因为你手头上的样本已经实现了,其发生概率最大才符合逻辑。这时是求样本所有观测的联合概率最大化,是个连乘积,只要取对数,就变成了线性加总。此时通过对参数求导数,并令一阶导数为零,就可以通过解方程(组),得到最大似然估计值。
最小二乘:找到一个(组)估计值,使得实际值与估计值的距离最小。本来用两者差的绝对值汇总并使之最小是最理想的,但绝对值在数学上求最小值比较麻烦,因而替代做法是,找一个(组)估计值,使得实际值与估计值之差的平方加总之后的值最小,称为最小二乘。“二乘”的英文为 least square,其实英文的字面意思是“平方最小”。这时,将这个差的平方的和式对参数求导数,并取一阶导数为零,就是 OLSE。
57、最大似然和损失函数
MLE 相当于找到训练数据集似然度(或等效对数似然度)最大时的参数θ。更具体的来说,下图的表述得到了最大化:
当 p(Y|X,θ)由模型确定时,它表示了训练数据中真实标签的概率。如果 p(Y|X,θ) 接近于 1,这意味着模型能够确定训练集中正确的标签/均值。在给定由 N 个观察对组成的训练数据(X,Y)的条件下,训练数据的似然度可被改写成对数概率的总和。
在分类与回归的情况下,p(y|x,θ)作为一个(x,y)的后验概率,可以被改写成范畴分布和高斯分布。在优化神经网络的情况下,目标则是去改变参数,具体方式是:对于一系列输入 X,概率分布 Y 的正确的参数可以在输出(回归值或类)中得到。一般这可以通过梯度下降和其变体来实现。因此,为了得到一个 MLE 估计,我们的目标是优化关于真实输出的模型输出:
• 最大化一个范畴分布的对数值相当于最小化真实分布与其近似分布的交叉熵。
• 最大化高斯分布的对数值相当于最小化真实均值与其近似均值的均方差。
60、最大似然估计和网络训练
softmax 虽然是基于 multinoulli 分布,但是每次对测试样本进行预测的时候,输出的值是各个类的概率分布,即 P(c|x),但是该概率和我们基于的分布概率是不一样的。分布概率是通过求期望来评估的。
最大似然估计是是找到能最大化模型产生真实观察数据可能性的那一组参数。不管是分类还是回归,每个样本的输出都是模型对该样本的单独计算结果,是该样本的后验概率。对于类别 a,所有被判断为 a 的样本的后验概率加一起可以得到该类别的先验概率。模型的损失函
数都是基于最大似然估计,分类和回归的模型输出都输出的是模型该样本的所有类别的后验概率分布,回归之所以是一个值,是因为对高斯分布建模,直接选择均值作为输出,因为高斯分布的均值的概率是最高的
21、解释残差学习
深度网络容易造成梯度在 back propagation 的过程中消失,导致训练效果很差,而深度残差网络在神经网络的结构层面解决了这一问题,使得就算网络很深,梯度也不会消失。
对于神经网络来讲,我们需要通过反向传播来对网络的权重进行调整就像这样
这个偏导就是我们求的 gradient,这个值本来就很小,而且再计算的时候还要再乘stepsize,就更小了 所以通过这里可以看到,梯度在反向传播过程中的计算,如果 N 很大,那么梯度值传播到前几层的时候就会越来越小,也就是梯度消失的问题。那 DRN 是怎样解决这个问题的呢?它在神经网络结构的层面解决了这个问题 它将基本的单元改成了这个样子
这样就算深度很深,梯度也不会消失了。当然深度残差这篇文章最后的效果好,是因为还结合了调参数以及神经网络的其他的细节,这些也很重要,不过就不是这里我们关心的内容了。可以看到,对于相同的数据集来讲,残差网络比同等深度的其他网络表现出了更好的性能,收敛更快。
70、优化器的自适应解释
这里的自适应是针对不同的参数,自适应的调整学习率,有的参数更新次数较多,学习率就不断降低,更新次数少的参数,则学习率保持较大。
生成器是在 for 循环的过程中不断计算出下一个元素,并在适当的条件结束 for 循环。对于函数改成的生成器来说,遇到 return 语句或者执行到函数体最后一行语句,就是结束生成器的指令,for 循环随之结束。定义生成器是在函数中加入 yield,在每次调用 next()的时候执行,遇到 yield 语句返回,再次执行时从上次返回的 yield 语句处继续执行。
Python 的迭代器对象表示的是一个数据流,迭代器对象可以被 next()函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next()函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。生成器都是 Iterator 对象,但 list、dict、str 虽然是 Iterable,却不是 Iterator。把 list、dict、str 等 Iterable 变成 Iterator 可以使用 iter()函数。
在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,装饰器就是一个返回函数的高阶函数。
1、生成器Generator
数据量加大时,列表会占用很大的存储空间,如果只需要访问前几个元素,这样就会造成极大的资源浪费。
所以,如果有一种对象,存储的不是具体的列表元素,而是计算列表元素的方法,在我们需要的时候,可以计算出相应的元素,那么资源浪费的问题就结局了。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
generator保存的是算法,我们可以每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,或者可以通过for循环来遍历generator
1.1 生成器表达式"()"
将列表生成式中[]改成() 之后列表变为生成器
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。因此,没有必要创建完整的列表(节省大量内存空间)。在Python中,我们可以采用生成器:边循环,边计算的机制—>generator
L = [x * x for x in range(10)]
g = (x * x for x in range(10)) # 创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator
L,g
# 输出
([0, 1, 4, 9, 16, 25, 36, 49, 64, 81],
at 0x106ccb678>)
1.2 方法体里加上yield关键词
如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。
generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
2、可以直接作用于for循环的对象统称为可迭代对象Iterable,主要有两种类型:一类是集合数据类型,如list、tuple、dict、set、str等;
一类是generator,包括生成器表达式和带yield的generator function。
3、迭代器Iterator
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。 不过把list、dict、str等Iterable变成Iterator可以使用iter()函数。
from collections import Iterator
print(isinstance((x for x in range(10)), Iterator)) # True
print(isinstance([], Iterator)) # False
print(isinstance({}, Iterator)) # False
print(isinstance('abc',Iterator)) # False
print(isinstance(iter('abc'), Iterator)) # True
你可能会问,为什么list、dict、str等数据类型不是Iterator?这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
4、Python的for循环本质上就是通过不断调用next()函数实现的。
5、总结
1、凡是可作用于for循环的对象都是Iterable类型;包括集合数据类型和Generator。
2、凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
3、集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。
迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身,它就是可迭代的。对于 string、list、dict、tuple 等这类容器对象,使用for循环遍历是很方便的,for 语句实际上会对容器对象调用 iter() 函数。iter() 会返回一个定义了 next() 方法的迭代器对象,它在容器中逐个访问容器内元素,在没有后续元素时,next()会抛出一个StopIteration异常。
迭代器是遵循迭代协议的对象。用户可以使用 iter() 以从任何序列得到迭代器(如 list, tuple, dictionary, set 等)。另一个方法则是创建一个另一种形式的迭代器 —— generator 。要获取下一个元素,则使用成员函数 next()
生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用yield语句。生成器能做到迭代器能做的所有事,而且因为自动创建iter()和next()方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出StopIteration异常。
生成器(Generator),只是在需要返回数据的时候使用yield语句。每次next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)
python生成器是一个返回可以迭代对象的函数,可以被用作控制循环的迭代行为。生成器类似于返回值为数组的一个函数,这个函数可以接受参数,可以被调用,一般的函数会返回包括所有数值的数组,生成器一次只能返回一个值,这样消耗的内存将会大大减小。
解释生成器(generator)与函数的不同?
生成器和函数的主要区别在于函数 return avalue,生成器 yield a value同时标记或记忆point of the yield 以便于在下次调用时从标记点恢复执行。 yield 使函数转换成生成器,而生成器反过来又返回迭代器。
yield就是保存当前程序执行状态。你用for循环的时候,每次取一个元素的时候就会计算一次。用yield的函数叫generator,和iterator一样,它的好处是不用一次计算所有元素,而是用一次算一次,可以节省很多空间,generator每次计算需要上一次计算结果,所以用yield,否则一return,上次计算结果就没了
yield简单说来就是一个生成器,这样函数它记住上次返 回时在函数体中的位置。对生成器第 二次(或n 次)调用跳转至该函 次)调用跳转至该函数。
在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,decorator就是一个返回函数的高阶函数。
所以,假设我们要定义一个能够打印当前函数名的decorator:
def log(func):
def wrapper(*args,**kwargs):
print('call %s()' % func.__name__)
return func(*args,**kwargs)
return wrapper
@log
def helloworld():
print('hello world')
helloworld()
执行结果为:
call helloworld()
hello world
下面的例子中,正是由于wrapper中把 func(args,*kwargs)进行return,因此函数得以执行。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
def log(text):
def decorator(func):
def wrapper(*args,**kwargs):
print('%s %s():' % (text,func.__name__))
return func(*args,**kwargs)
return wrapper
return decorator
@log('execute')
def helloworld():
print('hello world')
helloworld()
上面代码的执行过程相当于helloworld = log('execute')(helloworld)
我们讲了函数也是对象,它有name等属性,但你去看经过decorator装饰之后的函数,它们的name已经从原来的'helloworld'变成了'wrapper':
helloworld.__name__ #'wrapper'
如果需要把原始函数的name等属性复制到wrapper()函数中,使用Python内置的functools.wraps函数,代码如下:
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def helloworld():
print('hello world')
helloworld.__name__
此时的输出就变为了'helloworld'
对装饰器的理解,你能写出一个计时器装饰器,它能记录函数的执行时间吗?
装饰器本质上是一个callable object,它可以在让其他函数在不需要做任何代码的变动的前提下增加额外的功能。装饰器的返回值也是一个函数的对象,它经常用于有切面需求的场景。比如:插入日志,性能测试,事务处理,缓存。权限的校验等场景,有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。 详细参考:https://manjusaka.itscoder.com/2018/02/23/something-about-decorator/
装饰器的作用和功能:
import time
def timeit(func):
def wrapper():
start = time.clock()
func()
end = time.clock()
print('used:',end-start)
return wrapper
@timeit
def foo():
print('in foo()'foo())
有关于具体的装饰器的用法看这里:装饰器 - 廖雪峰的官方网站
闭包
在函数内部再定义一个函数,并且内嵌函数用到了外部函数的变量,那么将这个函数以及用到的一些变量称之为闭包。
创建一个闭包必须满足以下几点:
def line_conf(a, b):
def line(x):
return a*x + b
return line
line1 = line_conf(2, 3) # y=2x+3
print(line1(5))
函数line与变量a,b构成闭包,通过line_conf的参数a,b说明了这两个变量的取值,确定函数的最终形式y = 2x + 3,可以提高代码的复用性
如果没有闭包,每次创建直线函数时同时说明a,b,x,需要更多的参数传递,也减少了代码的可移植性。
python
python中dict和list的区别,dict的内部实现
dict查找速度快,占用的内存较大,list查找速度慢,占用内存较小,dict不能用来存储有序集合。Dict用{}表示,list用[]表示。
dict是通过hash表实现的,dict为一个数组,数组的索引键是通过hash函数处理后得到的,hash函数的目的是使键值均匀的分布在数组中。
python dict按照value进行排序
按照value从大到小排序:
sorted(d.items(),key = lambda x:x[1],reverse = True)
按照value从小到大排序:
sorted(d.items(),key = lambda x:x[1],reverse =False)
Python多进程
方式一: os.fork()
方式二:使用multiprocessing模块:创建Process的实例,传入任务执行函数作为参数
方式三:使用multiprocessing模块:派生Process的子类,重写run方法
方式四:使用进程池Pool
Python中的各种锁:
一、全局解释器锁(GIL)
1、什么是全局解释器锁
每个CPU在同一时间只能执行一个线程,那么其他的线程就必须等待该线程的全局解释器,使用权消失后才能使用全局解释器,即使多个线程直接不会相互影响在同一个进程下也只有一个线程使用cpu,这样的机制称为全局解释器锁(GIL)。GIL的设计简化了CPython的实现,使的对象模型包括关键的内建类型,如:字典等,都是隐含的,可以并发访问的,锁住全局解释器使得比较容易的实现对多线程的支持,但也损失了多处理器主机的并行计算能力。
2、全局解释器锁的好处
1)、避免了大量的加锁解锁的好处
2)、使数据更加安全,解决多线程间的数据完整性和状态同步
3、全局解释器的缺点
多核处理器退化成单核处理器,只能并发不能并行。
4、GIL的作用:
多线程情况下必须存在资源的竞争,GIL是为了保证在解释器级别的线程唯一使用共享资源(cpu)。
二、同步锁
1、什么是同步锁?
同一时刻的一个进程下的一个线程只能使用一个cpu,要确保这个线程下的程序在一段时间内被cpu执,那么就要用到同步锁。
2、为什么用同步锁?
因为有可能当一个线程在使用cpu时,该线程下的程序可能会遇到io操作,那么cpu就会切到别的线程上去,这样就有可能会影响到该程 序结果的完整性。
3、怎么使用同步锁?
只需要在对公共数据的操作前后加上上锁和释放锁的操作即可。
4、同步锁的所用:
为了保证解释器级别下的自己编写的程序唯一使用共享资源产生了同步锁。
三、死锁
1、什么是死锁?
指两个或两个以上的线程或进程在执行程序的过程中,因争夺资源或者程序推进顺序不当而相互等待的一个现象。
2、死锁产生的必要条件?
互斥条件、请求和保持条件、不剥夺条件、环路等待条件
3、处理死锁的基本方法?
预防死锁、避免死锁(银行家算法)、检测死锁(资源分配)、解除死锁:剥夺资源、撤销进程
四、什么是递归锁?
在Python中为了支持同一个线程中多次请求同一资源,Python提供了可重入锁。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。递归锁分为可递归锁与非递归锁。
五、什么是乐观锁?
假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
六、什么是悲观锁?
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
七、python常用的加锁方式?
互斥锁、可重入锁、迭代死锁、互相调用死锁、自旋锁。
python方法解析顺序
Python的方法解析顺序优先级从高到低为:实例本身类继承类(继承关系越近,越先定义,优先级越高)
进程和线程以及它们的区别
进程是对运行时程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发(如:用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等,然后该进程被放入到进程的就绪队列,进程调度程序选中它,为它分配CPU及其他相关资源,该进程就被运行起来);
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;
在没有实现线程的操作系统中,进程既是资源分配的基本单位,又是调度的基本单位,它是系统中并发执行的单元。而在实现了线程的操作系统中,进程是资源分配的基本单位,但是线程是调度的基本单位,是系统中并发执行的单元。
引入线程主要有以下4个方面的优点:
1)易于调度。
2)提高并发性。通过线程可以方便有效地实现并发。
3)开销小。创建线程比创建进程要快,所需要的开销也更小。
4)有利于发挥多处理器的功能。通过创建多线程,每个线程都在一个处理器上运行,从而实现应用程序的并行,使每个处理器都得到充分的运行。
尽管线程和进程很相似,但两者也存在着很大的不同,区别如下:
一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存空间。
属于一个进程的所有线程共享该进程的所有资源,包括打开的文件,创建的Socket等。不同的进程互相独立。
线程又被称为轻量级进程。进程有进程控制块,线程有线程控制块。但线程控制块比进程控制块小得多。线程间切换代价小,进程间切换代价大。
进程是程序的一次执行,线程可以理解为程序中一段程序片段的执行。
new/delete与malloc/free之间的区别?
什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?
内存泄漏:动态分配内存开辟的空间在使用完毕后未手动释放,一直占据该内存,可能导致程序运行速度减慢甚至系统崩溃等严重后果。
解决方法:
指针和引用的区别
STL库用过吗?常见的STL容器有哪些?算法用过几个?
STL包括两部分内容:容器和算法
容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器
序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist
关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set
算法有排序,复制等,以及各个容器特定的算法
迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。
STL中的vector的实现和扩容
vector是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会以原大小的1.5倍或2倍申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。
STL中unordered_map和map的区别
map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。
unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。
C++中vector和list的区别
vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。
vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector
list的内存空间可以是不连续,它不支持随机访问,因此list
vector
总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。
静态语音和动态语言
C和C++的区别
C++模板
一个模板就是一个编译器用来生成特定类类型或函数的蓝图。生成特定类或函数的过程称为实例化。我们只编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。
我们既可以定义函数模板,也可以定义类模板。标准库算法都是函数模板,标准库容器都是模板类。
函数模板
template
类模板
template
C++的内存管理
C++程序编译的内存分配
delete和delete[]的区别
delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数;用new分配的内存用delete释放,用new[]分配的内存用delete[]释放
深拷贝和浅拷贝的区别
如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。
浅拷贝:简单的赋值拷贝操作;深拷贝:在堆区重新申请空间,进行拷贝操作
C++ 中struct和class
struct常作为数据结构的实现体,class常作为对象的实现体,最本质区别是默认的访问权限不同,struct默认为公有的public,class默认为私有private。
声明和定义的区别
声明是告诉编译器变量的类型和名字,不会为变量分配空间;定义需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
野指针
野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。关键字:public, protected, private。不写默认为 private。
public
成员:可以被任意实体访问protected
成员:只允许被子类及本类的成员函数访问private
成员:只允许被本类的成员函数访问继承
this
指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐含使用 this
指针。this
指针被隐含地声明为: ClassName *const this
,这意味着不能给 this
指针赋值;在 ClassName
类的 const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能对 this
指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);this
并不是一个常规变量,而是个右值,所以不能取得 this
的地址(不能 &this
)。this
指针:
list
。生活中你的家有客厅(Public),有你的卧室(Private)客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去
但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术友元的目的就是让一个函数或者类 访问另一个类中私有成员;友元的关键字为friend
友元的三种实现
全局函数做友元
类做友元
成员函数做友元
friend 友元类和友元函数
友元函数和友元类
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。
通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
1)友元函数
有元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。
friend 类型 函数名(形式参数);
一个函数可以是多个类的友元函数,只需要在各个类中分别声明。
2)友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
friend class 类名;
使用友元类时注意:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
C++中static关键字的作用
同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,所以加了static关键字的变量和函数可对其它源文件隐藏。还可以保持变量内容的持久性。用static前缀作为关键字的变量默认的初始值为0。
1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值
2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内
3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝
4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量
注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象
C++中的基本数据类型及派生类型
1)整型 int
2)浮点型 单精度float,双精度double
3)字符型 char
4)逻辑型 bool
5)控制型 void
基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。
类型修饰符包括:
>short 短类型,缩短字长
>long 长类型,加长字长
>signed 有符号类型,取值范围包括正负值
>unsigned 无符号类型,取值范围只包括正值
C++文件编译与执行的四个阶段
构造函数、析构函数
生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供
编译器提供的构造函数和析构函数是空实现。
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:类名(){}
构造函数,没有返回值也不写void
函数名称与类名相同
构造函数可以有参数,因此可以发生重载
程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
析构函数,没有返回值也不写void
函数名称与类名相同,在名称前加上符号 ~
析构函数不可以有参数,因此不可以发生重载
程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
什么情况下会调用拷贝构造函数(三种情况)
系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)
生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数
调用拷贝构造函数的情形:
1)用类的一个对象去初始化另一个对象的时候
2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候
举例:
#include
#include
using namespace std;
class A{
private:
int data;
public:
A(int i){ data = i;} //自定义的构造函数
A(A && a); //拷贝构造函数
int getdata(){return data;}
};
//拷贝构造函数
A::A(A && a){
data = a.data;
cout <<"拷贝构造函数执行完毕"<
4.7.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
多态满足条件
有继承关系
子类重写父类中的虚函数
多态使用条件
父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为==抽象类==
抽象类特点:
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
多态, 虚函数, 纯虚函数
多态:不同对象接收相同的消息产生不同的动作。多态包括 编译时多态(运算符重载)和 运行时多态(继承和虚函数)。
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了代码重用。多态也有代码重用的功能,还有解决项目中紧耦合的问题,提高程序的可扩展性。
C++实现多态的机制很简单,在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个虚函数进行重写,利用父类的指针或引用调用虚函数。通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。对于虚函数调用来说,每一个对象内部都有一个虚表指针,在构造子类对象时,执行构造函数中进行虚表的创建和虚表指针的初始化,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
需要注意的几点总结(基类有虚函数):
1、每一个类都有虚表,单继承的子类拥有一张虚表,子类对象拥有一个虚表指针;若子类是多重继承(同时继承多个基类),则子类维护多张虚函数表(针对不同基类构建不同虚表),该子类的对象也将包含多个虚表指针。
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
第一:编译器在发现Father 类中有虚函数时,会自动为每个含有虚函数的类生成一份虚函数表,也叫做虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址。
第二:编译器会在每个对象的前四个字节中保存一个虚表指针,即(vptr),指向对象所属类的虚表。在程序运行时的合适时机,根据对象的类型去初始化vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。
第三:所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。
虚函数: 在基类中用virtual的成员函数。允许在派生类中对基类的虚函数重新定义。
基类的虚函数可以有函数体,基类也可以实例化。
虚函数要有函数体,否则编译过不去。
虚函数在子类中可以不覆盖。
构造函数不能是虚函数。
纯虚函数:基类中为其派生类保留一个名字,以便派生类根据需要进行定义。
包含一个纯虚函数的类是抽象类。
纯虚函数后面有 = 0;
抽象类不可以实例化。但可以定义指针。
如果派生类如果不是先基类的纯虚函数,则仍然是抽象类。
抽象类可以包含虚函数。
虚函数是怎么实现的
每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),
类的示例对象不包含虚函数表,只有虚指针;
派生类会生成一个兼容基类的虚函数表。
纯虚函数
纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承
包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象
普通函数是静态编译的,没有运行时多态
虚函数、纯虚函数
虚函数和纯虚函数的区别
含有纯虚函数的类称为抽象类,只含有虚函数的类不能称为抽象类。虚函数可以直接被使用,也可以被子类重载以后以多态形式调用,而纯虚函数必须在子类中实现该函数才可使用,因为纯虚函数在基类中只有声明而没有定义。虚函数必须实现,对虚函数来说父类和子类都有各自的版本。
构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?
1、构造函数不能声明为虚函数
1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等
2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
2、析构函数最好声明为虚函数
首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
子类析构时,要调用父类的析构函数吗?
析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。
不用显式调用,会自动调用
多态
静态多态(编译期/早绑定)
函数重载
class A
{
public:
void do(int a);
void do(int a, int b);
};
函数重载概述
作用:函数名可以相同,提高复用性
函数重载满足条件:
同一个作用域下
函数名称相同
函数参数类型不同 或者 个数不同 或者 顺序不同
注意: 函数的返回值不可以作为函数重载的条件
动态多态(运行期期/晚绑定)
注意:
动态多态使用
class Shape // 形状类
{
public:
virtual double calcArea()
{
...
}
virtual ~Shape();
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
class Rect : public Shape // 矩形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
shape1->calcArea(); // 调用圆形类里面的方法
shape2->calcArea(); // 调用矩形类里面的方法
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
虚析构函数使用
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
virtual int A() = 0;
纯虚函数怎么定义
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。它的一般格式如下:
class <类名>
{
virtual <类型><函数名>(<参数表>)=0;
…
};
CSDN . C++ 中的虚函数、纯虚函数区别和联系
.rodata section
,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
安全类型转换
智能指针
什么是智能指针?智能指针的原理
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,
智能指针就是一种栈上创建的对象,函数退出时会调用其析构函数,这个析构函数里面往往就是一堆计数之类的条件判断,如果达到某个条件,就把真正指针指向的空间给释放了。
注意事项:不能将指针直接赋值给一个智能指针,一个是类,一个是指针。
常用的智能指针
智能指针在C++11版本之后提供,包含在头文件
1)std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃,比如
auto_ptr p1(new string ("auto") ; //#1
auto_ptr p2; //#2
p2 = p1; //#3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象;
但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。如果再访问p1指向的内容则会导致程序崩溃。
auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。
2) C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。例如:
std::unique_ptr p1(new int(5)) // #4
std::unique_ptr p2 = p1; // 编译会出错 //#5
std::unique_ptr p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针. //#6
编译器认为语句#5非法,因此,unique_ptr比auto_ptr更安全。
但unique_ptr还有更聪明的地方。 有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做
unique_ptr pu1(new string ("hello world"));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
3) C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。
4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。
智能指针的作用
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存
C++ 标准库(STL)中
头文件:#include
C++ 11
shared_ptr
多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。
weak_ptr
weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。
unique_ptr
unique_ptr 是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
auto_ptr
被 c++11 弃用,原因是缺乏语言特性如 “针对构造和赋值” 的 std::move
语义,以及其他瑕疵。
auto_ptr 与 unique_ptr 比较
move
语义;delete
),unique_ptr 可以管理数组(析构调用 delete[]
);在程序里面智能指针的名字是啥?
一个是shared_ptr允许多个指针指向同一个对象;一个是unique_ptr独占所指向的对象;还有一种伴随类weak_ptr他是一种弱引用 指向shared_ptr所指向的对象。
多继承
显示转换
C++的四种强制转换
类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)
(new-type) expression
new-type (expression)
隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:
static_cast、dynamic_cast、const_cast、reinterpret_cast
1)static_cast :编译时期的静态类型检查
static_cast < type-id > ( expression )
该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的
2)dynamic_cast:运行时的检查
用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用
dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。
dynamic_cast如果不能转换返回NULL
dynamic_cast转为引用类型的时候转型失败会抛bad_cast
源类中必须要有虚函数,保证多态,才能使用dynamic_cast
3)const_cast
去除const常量属性,使其可以修改 ; volatile属性的转换
4)reinterpret_cast
通常为了将一种数据类型转换成另一种数据类型
MSDN . 强制转换运算符
static_cast
向上转换是一种隐式转换。
dynamic_cast
const_cast
reinterpret_cast
char*
到 int*
或 One_class*
到 Unrelated_class*
之类的转换,但其本身并不安全)bad_cast
bad_cast 使用
try {
Circle& ref_circle = dynamic_cast(ref_shape);
}
catch (bad_cast b) {
cout << "Caught: " << b.what();
}
重载的问题(参数列表不同)返回值不同可不可以重载
三个关键字的访问权限与继承权限的区别
lambda表达式
深拷贝,浅拷贝,写一个出来(写了个自己认为对的版本)
深拷贝与浅拷贝的区别
深复制和浅复制最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。
浅复制 —-只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深复制 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。
浅拷贝实例
//此递归方法不包含数组对象
var obj = { a:1, arr: [2,3] };
var shallowObj = shallowCopy(obj);
function shallowCopy(src) {
var newobj = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
newobj[prop] = src[prop];
}
}
return newobj;
}
深拷贝实例
而深复制则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。这就不会存在上面obj和shallowObj的arr属性指向同一个对象的问题。
var obj = {
a:1,
arr: [1,2],
nation : '中国',
birthplaces:['北京','上海','广州']
};
var obj2 = {name:'杨'};
1 2 |
|
//深复制,要想达到深复制就需要用递归
function deepCopy(o,c){
var c = c || {};
for(var i in o){
if(typeof o[i] === 'object'){
//要考虑深复制问题了
1 |
|
//这是数组
1 2 |
|
//这是对象
1 2 3 4 5 6 7 8 9 |
|
写一个函数指针
1 2 |
|
int compute(int a, int b, int(*func)(int, int)) { //函数指针
return func(a, b);
}
int max(int a, int b) {
return (a > b) ? a : b;
}
int min(int a, int b) {
return(a < b) ? a : b;
}
int add(int a, int b) {
return a + b;
}
int main() {
int m, n, res_max, res_min, res_add;
类型 |
abstract class |
Interface |
定义 |
abstract class关键字 |
Interface关键字 |
继承 |
抽象类可以继承一个类和实现多个接口;子类只可以继承一个抽象类 |
接口只可以继承接口(一个或多个);子类可以实现多个接口 |
访问修饰符 |
抽象方法可以有public、protected和default这些修饰符 |
接口方法默认修饰符是public。 不可以使用其它修饰符 |
方法实现 |
可定义构造方法,可以有抽象方法和具体方法 |
接口完全是抽象的,没构造方法,且方法都是抽象的,不存在方法的实现 |
实现方式 |
子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 |
子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 |
作用 |
了把相同的东西提取出来,即重用 |
为了把程序模块进行固化的契约,是为了降低偶合 |
1 2 3 4 5 6 7 |
|
框架
pytorch中cuda()作用,两个Tensor,一个加了cuda(),一个没加,相加后很怎样?
cuda()将操作对象放在GPU内存中,加了cuda()的Tensor放在GPU内存中而没加的Tensor放在CPU内存中,所以这两个Tensor相加会报错。
const 指针和指针 const
const int * pOne; //指向 整形常量 的指针,它指向的值不能修改
int * const pTwo; //指向整形的 常量指针 ,它不能在指向别的变量,但指向(变量)的值可以修改。
函数后面接const是什么意思?
这是把整个函数修饰为const,意思是“函数体内不能对成员数据做任何改动”。如果你声明这个类的一个const实例,那么它就只能调用有const修饰的函数
const知道吗?解释一下其作用
const修饰类的成员变量,表示常量不可能被修改
const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数
const函数只能调用const函数,非const函数可以调用const函数
作用
使用
const 使用
// 类
class A
{
private:
const int a; // 常对象成员,只能在初始化列表赋值
public:
// 构造函数
A() : a(0) { };
A(int x) : a(x) { }; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数
int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值
};
void function()
{
// 对象
A b; // 普通对象,可以调用全部成员函数
const A a; // 常对象,只能调用常成员函数、更新常成员变量
const A *p = &a; // 常指针
const A &q = a; // 常引用
// 指针
char greeting[] = "Hello";
char* p1 = greeting; // 指针变量,指向字符数组变量
const char* p2 = greeting; // 指针变量,指向字符数组常量
char* const p3 = greeting; // 常指针,指向字符数组变量
const char* const p4 = greeting; // 常指针,指向字符数组常量
}
// 函数
void function1(const int Var); // 传递过来的参数在函数内不可变
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为常指针
void function4(const int& Var); // 引用参数在函数内为常量
// 函数返回值
const int function5(); // 返回一个常数
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
this指针概念
通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
当形参和成员变量同名时,可用this指针来区分
在类的非静态成员函数中返回对象本身,可使用return *this
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
//抽象制作饮品
class AbstractDrinking {
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入枸杞!" << endl;
}
};
//业务函数
void DoWork(AbstractDrinking* drink) {
drink->MakeDrink();
delete drink;
}
void test01() {
DoWork(new Coffee);
cout << "--------------" << endl;
DoWork(new Tea);
}
int main() {
test01();
system("pause");
return 0;
}
解释 densenet block
在该网络中,任何两层之间都有直接的连接,也就是说,网络每一层的输入都是前面所有层输出的并集,而该层所学习的特征图也会被直接传给其后面所有层作为输入。下图是DenseNet 的一个 dense block 示意图,一个 block 里面的结构如下,与 ResNet 中的BottleNeck 基本一致:BN-ReLU-Conv(1×1)-BN-ReLU-Conv(3×3) (因为 BN 和 ReLU 是对前面 concat 的卷积后的特征图进行计算的),而一个 DenseNet 则由多个这种 block 组成。每 个 DenseBlock 的 之 间 层 称 为 transition layers , 由 BN − >Conv(1 × 1)
−>averagePooling(2×2)组成
densenet 缺点
更少的参数量并不意味着更节约显存或训练每一步会更快。因为有稠密直连的过程,所以各
个 feature 都要存下来,实际上很容易爆显存,另外,这种稠密连接也意味着反向传播计算
梯度更加复杂,每一步训练并不一定会更快。
最大似然估计提供了一种给定观察数据来评估模型参数的方法,而最大似然估计中的采样满足所有采样都是独立同分布的假设。最大后验概率是根据经验数据获难以观察量的点估计,与最大似然估计最大的不同是最大后验概率融入了要估计量的先验分布在其中,所以最大后验概率可以看做规则化的最大似然估计。
概率是指在给定参数的情况下,样本的随机向量X=x的可能性。而似然表示的是在给定样本X=x的情况下,参数为真实值的可能性。一般情况,对随机变量的取值用概率表示。而在非贝叶斯统计的情况下,参数为一个实数而不是随机变量,一般用似然来表示。
频率派认为抽样是无限的,在无限的抽样中,对于决策的规则可以很精确。贝叶斯派认为世界无时无刻不在改变,未知的变量和事件都有一定的概率,即后验概率是先验概率的修正。频率派认为模型参数是固定的,一个模型在无数次抽样后,参数是不变的。而贝叶斯学派认为数据才是固定的而参数并不是。频率派认为模型不存在先验而贝叶斯派认为模型存在先验。
卷积神经网络(Convolutional Neural Networks,CNN)
卷积神经网络是一类包含卷积、激活、池化、全连接四种基本操作且具有深度结构的前馈神经网络,主要由两部分组成,一部分是卷积、激活、池化完成对输入图片进行特征提取,另一部分是全连接网络对提取后的特征信息进行分类或回归计算。
注意:二分类使用sigmoid激活函数,多分类使用Softmax激活函数,回归问题使用没有激活函数的单个神经元的线性层,网络可以学习任意范围内的值。卷积层有激活函数,池化层没有激活函数。CNN与DNN主要不同在于卷积层和池化层。
CNN中的卷积层
卷积层(特征提取层):前一层的特征图与一个可学习的卷积核进行卷积运算,卷积的结果加上偏置经过激活函数后的输出形成这一层的神经元,以一定步长遍历整幅图像构成该层特征图(feature map),每个神经元不必感受整幅图像,只需要感受局部,每个卷积核学习一种特征,每个卷积层需要多个卷积核来学习多种不同的特征,一组固定的参数和不同窗口做卷积(将局部感受野位于不同位置的神经元设置为相同的权值)。
卷积操作的本质特性包括稀疏交互和参数共享:
稀疏交互(Sparse Interaction)
层与层之间的稀疏连接减少计算复杂度
对于全连接网络,每个神经元与前后相邻层的每一个神经元都有连接关系,每个神经元节点响应前一层的全部节点,形成稠密的连接结构,如图,神经元si 与输入的所有神经元xj 均有连接。
CNN中卷积核尺度远小于输入的维度,这样每个输出神经元仅与前一层特定局部区域内的神经元存在连接权重(即产生交互),我们称CNN每层的神经元节点只响应前一层局部区域范围内的神经元的特性为稀疏交互,与稠密的连接结构不同,神经元相连。
具体来讲,假设网络中相邻两层分别具有m个输入和n个输出,全连接网络中的权值参数矩阵将包含m×n个参数。对于稀疏交互的卷积网络,如果限定每个输出与前一层神经元的连接数为k,那么该层的参数总量为k×n。在实际应用中,一般k值远小于m就可以取得较为可观的效果;而此时优化过程的时间复杂度将会减小几个数量级,过拟合的情况也得到了较好的改善。
稀疏交互的物理意义是,通常图像、文本、语音等现实世界中的数据都具有局部的特征结构,我们可以先学习局部的特征,再将局部的特征组合起来形成更复杂和抽象的特征。以人脸识别为例,最底层的神经元可以检测出各个角度的边缘特征(a);位于中间层的神经元可以将边缘组合起来得到眼睛、鼻子、嘴巴等复杂特征(b);最后,位于上层的神经元可以根据各个器官的组合检测出人脸的特征(c)。
人脸识别中不同卷积层的可视化
参数共享(Parameter Sharing)
卷积使得神经网络可以共享权值,一方面减少了参数,另一方面可以学习图像不同位置的局部特征.
参数共享是卷积运算的固有属性,指在同一个模型的不同模块中使用相同的参数,这使得需要优化的参数数目大大缩减,提高了模型的训练效率以及可扩展性。全连接网络中,计算每层的输出时,权值参数矩阵中的每个元素只作用于某个输入元素一次;而在卷积神经网络中,卷积核中的每一个元素将作用于每一次局部输入的特定位置上。根据参数共享的思想,我们只需要学习一组参数集合,而不需要针对每个位置的每个参数都进行优化,从而大大降低了模型的存储需求。
参数共享的物理意义是使得卷积层具有平移等变性。假如图像中有一只猫,那么无论它出现在图像中的任何位置,我们都应该将它识别为猫,也就是说神经网络的输出对于平移变换来说应当是等变的。
特别地,当函数f(x)与g(x)满足f(g(x))=g(f(x))时,我们称f(x)关于变换g具有等变性。将g视为输入的任意平移函数,令I表示输入图像(在整数坐标上的灰度值函数),平移变换后得到I’=g(I)。例如,我们把猫的图像向右移动l像素,满足I’(x,y)=I(x−l,y)。我们令f表示卷积函数,根据其性质,我们很容易得到g(f(I))=f(I’)=f(g(I))。也就是说,在猫的图片上先进行卷积,再向右平移l像素的输出,与先将图片向右平移l像素再进行卷积操作的输出结果是相等的。
CNN中的池化层
把输入信号分割成不重叠的区域,对于每个区域通过池化(下采样)运算来降低网络的空间分辨率。
最大值池化(max pooling):
最大值池化则通过取邻域内特征的最大值来实现,能够抑制网络参数误差造成估计均值偏移的现象,特点是更好地提取纹理信息。
均值池化(mean pooling):
均值池化通过对邻域内特征数值求平均来实现,能够抑制由于邻域大小受限造成估计值方差增大的现象,特点是对背景的保留效果更好。
此外,特殊的池化方式还包括对相邻重叠区域的池化以及空间金字塔池化。相邻重叠区域的池化是采用比窗口宽度更小的步长,使得窗口在每次滑动时存在重叠的区域。空间金字塔池化主要考虑了多尺度信息的描述,例如同时计算1×1、2×2、4×4的矩阵的池化并将结果拼接在一起作为下一网络层的输入。
池化操作除了能显著降低参数量外,还能够保持对平移、伸缩、旋转操作的不变性。平移不变性是指输出结果对输入的小量平移基本保持不变。例如,输入为(1,5,3),最大池化将会取5,如果将输入右移一位得到(0,1,5),输出的结果仍将为5。对伸缩的不变性(尺度不变性)如果原先神经元在最大池化操作之后输出5,那么在经过伸缩(尺度变换)之后,最大池化操作在该神经元上很大概率的输出仍然是5。因为神经元感受的是邻域输入的最大值,而并非某一个确定的值。旋转不变性可以参照图9.19。图中的神经网络由3个学得的过滤器和一个最大池化层组成。这3个过滤器分别学习到不同旋转方向的“5”。当输入中出现“5”时,无论进行何种方向的旋转,都会有一个对应的过滤器与之匹配并在对应的神经元中引起大的激活。最终,无论哪个神经元获得了激活,在经过最大池化操作之后输出都会具有大的激活。
池化操作的旋转不变性
模型优缺点
优点:
缺点:
CNN前向传播算法
NN的前向传播算法,重点是输入层的前向传播,卷积层的前向传播以及池化层的前向传播。而DNN全连接层和用Softmax激活函数的输出层的前向传播算法与DNN相同。
CNN反向传播算法
以最基本的批量梯度下降法为例来描述反向传播算法。
输入:m个图片样本,CNN模型的层数L和所有隐藏层的类型,对于卷积层,要定义卷积核的大小K,卷积核子矩阵的维度F,填充大小P,步幅S。对于池化层,要定义池化区域大小k和池化标准(MAX或Average),对于全连接层,要定义全连接层的激活函数(输出层除外)和各层的神经元个数。梯度迭代参数迭代步长α,最大迭代次数MAX与停止迭代阈值ϵ
输出:CNN模型各隐藏层与输出层的W,b
gbdt的参数有哪些,如何调参 ?
Boosting 参数:
CART参数:
GBDT调参步骤:
朴素贝叶斯是基于贝叶斯定理与特征条件独立假设的分类方法。
贝叶斯定理是关于随机事件A和B的条件概率的定理,形式如下:
朴素贝叶斯分类的基本思想是:给出待分类项,求解在此项出现的条件下其他各个类别的出现的概率,哪个概率较大就认为待分类项属于哪个类别,用贝叶斯定理表示为(这里的上标表示一维特征):
分母对于所有的c都是相同的,因此可以省去,故有:
我们要做的就是统计对每一个类别来说,每一维特征每个特征出现的频率。频率有可能出现0的情况,我们需要进行*拉普拉斯平滑操作。
拉普拉斯平滑:就是对每类别下所有划分的计数加1,这样如果训练样本集数量充分大时,并不会对结果产生影响,并且解决了频率为0的尴尬局面。
import numpy as np
def get_dist(vec1, vec2): # 两个向量之间的欧几里德距离
return np.sqrt(np.sum(np.power(vec1 - vec2, 2)))
def rand_cent(dataSet, k): # 返回初始化得到的k个质心向量
n = np.shape(dataSet)[1] # 得到数据样本的维度
centroids = np.mat(np.zeros((k, n))) # 初始化为一个(k,n)的全零矩阵
# k个质心向量的第j维数据值随机为位于(最小值,最大值)内的某一值
for j in range(n): # 遍历数据集的每一个维度
minJ = np.min(dataSet[:, j]) # 得到该列数据的最小值,最大值
maxJ = np.max(dataSet[:, j])
rangeJ = float(maxJ - minJ) # 得到该列数据的范围(最大值-最小值)
centroids[:, j] = minJ + rangeJ * np.random.rand(k, 1) # k个在0 1之间的二维矩阵
return centroids # 返回初始化得到的k个质心向量
def kMeans(dataSet, k):
m = np.shape(dataSet)[0] # 获取数据集样本数
result = np.mat(np.zeros((m, 2))) # 初始化(m,2)全零矩阵 (属于哪一簇,距离质心距离)
centroids = rand_cent(dataSet, k) # 创建初始的k个质心向量
changed = True # 聚类结果是否发生变化的布尔类型
while changed: # 只要聚类结果一直发生变化,就一直执行聚类算法,直至所有数据点聚类结果不发生变化
changed = False # 聚类结果变化布尔类型置为False
for i in range(m): # 遍历数据集每一个样本向量
minDist = float('inf') # 初始化最小距离为正无穷,最小距离对应的索引为-1
minIndex = -1
for j in range(k): # 循环k个类的质心
dist = get_dist(dataSet[i, :], centroids[j, :]) # 计算数据点到质心的欧氏距离
if dist < minDist: # 如果距离小于当前最小距离
minDist = dist # 当前距离为最小距离,最小距离对应索引应为j(第j个类)
minIndex = j
if result[i, 0] != minIndex: # 当前聚类结果中第i个样本的聚类结果发生变化:布尔值置为True,继续聚类算法
changed = True
result[i, :] = minIndex, minDist**2 # 更新当前变化样本的聚类结果和平方误差
for cent in range(k): # 遍历每一个质心
points = dataSet[np.nonzero(result[:, 0].A == cent)[0]] # 所有属于当前质心类的样本
centroids[cent, :] = np.mean(points, axis=0) # 计算当前质心类的均值(axis=0:求列均值),作为该类质心向量
return centroids, result # 返回质心,属于哪一簇 距离质心距离
生成模型和判别模型
判别模型:输入x,直接建模P(y|x)来得到c
生成模型:先对P(y,x)进行建模,然后再由此获得P(y|x)
先验概率、条件概率/似然
类先验概率:p(y)
条件概率、似然:p(x|y)
朴素贝叶斯分类
目标函数:(其实是最小化分类错误率)
y=argmaxP(y=Ck)∏jP(Xj=xj|Y=ck)
y=argmaxP(y=Ck)∏jP(Xj=xj|Y=ck)
损失函数:0-1损失函数
基本假设:条件独立性(用于分类的特征在类确定的条件下都是条件独立的),解决组合爆炸、样本稀疏等问题
参数估计方法:极大似然估计和贝叶斯估计
极大似然估计:
试图在参数所有可能的取值中,找到一个能使数据出现的可能性最大的值。
对于离散属性而言
P(xi|yj)=Dxi,yjDyj
P(xi|yj)=Dxi,yjDyj
对于连续属性而言,可以考虑概率密度函数,假定p(xi|yj)∼N(μyj,xi,σ2yj,xi)p(xi|yj)∼N(μyj,xi,σyj,xi2),则
p(xi|yj)=12π−−√σi,jexp(−xi−μ2σ2)
p(xi|yj)=12πσi,jexp(−xi−μ2σ2)
如果给出的特征向量长度可能不同,这是需要归一化为通长度的向量(这里以文本分类为例),比如说是句子单词的话,则长度为整个词汇量的长度,对应位置是该单词出现的次数。
贝叶斯估计:
用极大似然估计可能会出现所要估计的概率值为0的情况,从而使得分类产生偏差。常用λ=1λ=1的拉普拉斯来进行修正,具体操作为,分子加上1,分母加上属于该类别的个数
P(yj)=|Dyj|+1|D|+N
P(yj)=|Dyj|+1|D|+N
优缺点
优点: 高效、易于训练。对小规模的数据表现很好,适合多分类任务,适合增量式训练。
缺点: 分类的性能不一定很高,对输入数据的表达形式很敏感。(离散、连续,值极大之类的)
半朴素贝叶斯
提出:现实任务中,条件独立性假设很难成立,于是,人们对属性独立性假设进行一定程度的放松。
想法:适当考虑一部分属性间的相互依赖信息,从而既不需进行联合概率计算,又不至于彻底忽略了比较强的属性依赖关系。
独依赖估计:属性最多依赖一个其他属性
SPODE:假设所有属性都依赖于一个属性
TAN:计算任意两个属性之间的条件互信息,构建最大带权生成树,TAN实际上仅保留了强相关属性之间的依赖性
AODE:尝试将每个属性作为超父来构建SPODE,集成SPODE作为最终结果
Q&A
Q: 为什么属性独立性假设在实际情况中很难成立,但朴素贝叶斯仍能取得较好的效果?
1)对于分类任务来说,只要各类别的条件概率排序正确、无需精准概率值即可导致正确分类;
2)如果属性间依赖对所有类别影响相同,或依赖关系的影响能相互抵消,则属性条件独立性假设在降低计算开销的同时不会对性能产生负面影响。(样本容量大了之后,属性类别之间的差异性会变得比较明显且趋于稳定。)
Q:贝叶斯分类器与贝叶斯学习不同:
前者:通过最大后验概率进行单点估计;后者:进行分布估计。
Q:Navie Bayes和Logistic回归区别是什么?
前者是生成式模型,后者是判别式模型,二者的区别就是生成式模型与判别式模型的区别。
1)首先,Navie Bayes通过已知样本求得先验概率P(Y), 及条件概率P(X|Y), 对于给定的实例,计算联合概率,进而求出后验概率。也就是说,它尝试去找到底这个数据是怎么生成的(产生的),然后再进行分类。哪个类别最有可能产生这个信号,就属于那个类别。
优点:样本容量增加时,收敛更快;隐变量存在时也可适用。
缺点:时间长;需要样本多;浪费计算资源
2)相比之下,Logistic回归不关心样本中类别的比例及类别下出现特征的概率,它直接给出预测模型的式子。设每个特征都有一个权重,训练样本数据更新权重w,得出最终表达式。梯度法。
优点:直接预测往往准确率更高;简化问题;可以反应数据的分布情况,类别的差异特征;适用于较多类别的识别。
缺点:收敛慢;不适用于有隐变量的情况。
xgboost是建树时逐层训练,在分裂节点时需要预排序,遍历每个分隔点计算增益,消耗空间较大,也不支持分类变量的自动处理。lightgbm建树时是面向叶节点的,可以支持分类型变量的处理,这就免去了对分类变量的one-hot等编码,减少了模型变量和数据量,同时使用直方图来加速计算。
最早的机器学习应用-垃圾邮件分类,传统的计算机解决问题的思路:编写规则,定义“垃圾邮件”,让计算机执行
缺点:很多问题规则很难定义,且很多规则在不断变化
特征也可以很抽象,如:图像的每一个像素点都是特征,28*28的图像有784个特征
非监督学习:
意义:对数据进行降维处理
对没有标记的数据进行分类---聚类分析
半监督学习:
一部分数据没有标记,另一部分有标记(各种原因导致的标签缺失)
处理方法:先使用无监督学习手段对数据做处理,再使用监督学习手段做模型的训练和预测
增强学习:
批量学习与在线学习:
奥卡姆剃刀:简单的就是好的
没有免费午餐定理:任意两个算法,它们的期望性能是相同的,算法的选择要具体问题具体分析
分类任务的本质实在特征空间切分。
使用训练数据训练模型,使用验证数据验证模型是否最优,如果误差较大,则调整模型参数,相当于使用训练数据和验证数据进行模型的创建,测试数据并没有参与模型的创建。
使用上述方法有一个缺点,只使用验证数据进行调参,有可能模型对验证数据过拟合,一旦验证数据有离群点,将会导致模型失效。解决方法是可以使用交叉验证解决。
超参数:在算法运行之前需要决定的参数。
模型参数:算法运行过程中要学习的参数。
KNN算法中没有模型参数,k,距离权重是的超参数
KNN中距离权重为:距离预测点更近的点具有更大的话语权,相应的拥有更大的权重,一般将1/距离作为权重
验证集:模型评估与选择中用于评估测试和模型调参的数据集。
测试集:在研究对比不同算法的泛化性能时,用测试集上的判别效果来估计模型在实际使用时的泛化能力,测试集应该尽可能与训练集互斥,即测试样本尽量不在训练集中出现、未在训练过程中使用过。
调参:对每个参数选定一个范围和变化步长,例如在[0,0.2]范围内以0.05为步长,则实际要评估的候选参数值有5个,最终是从这5个候选值中产生选定值。
根据切分的方法不同,交叉验证分为下面三种:
第一种是简单交叉验证。首先,随机的将样本数据分为两部分(比如: 70%的训练集,30%的测试集),然后用训练集来训练模型,在测试集上验证模型及参数。接着,再把样本打乱,重新选择训练集和测试集,继续训练数据和检验模型。最后我们选择损失函数评估最优的模型和参数。
第二种是S折交叉验证。S折交叉验证把样本数据随机的分成S份,每次随机的选择S-1份作为训练集,剩下的1份做测试集。当这一轮完成后,重新随机选择S-1份来训练数据。若干轮(小于S)之后,选择损失函数评估最优的模型和参数。
第三种是留一交叉验证,是第二种情况的特例,此时S等于样本数N,这样对于N个样本,每次选择N-1个样本来训练数据,留一个样本来验证模型预测的好坏。此方法主要用于样本量非常少的情况,比如对于普通适中问题,N小于50时,一般采用留一交叉验证。
ROC曲线:对角线对应于“随机猜测”模型,而点(0,1)则对应于将所有正例排在所有反例之前的“理想模型”。
绘图过程:给定m+个正例和m-个反例,根据学习器预测结果对样例进行排序,然后把分类阂值设为最大,即把所有样例均预测为反例,此时真正例率和假正例率均为0,在坐标(0, 0)处标记一个点。然后,将分类阈值依次设为每个样例的预测值,即依次将每个样例划分为正例。设前一个标记点坐标为(x,y),当前若为真正例,则对应标记点的坐标为 ;当前若为假正例,则对应标记点的坐标为 ,然后用线段连接相邻点即得。
进行学习器的比较时,若一个学习器的ROC曲线被另一个学习器的曲线完全“包住”,则可断言后者的性能优于前者;若两个学习器的ROC曲线发生交叉,则难以一般性地断言两者孰优孰劣.此时如果一定要进行比较,则较为合理的判据是比较ROC曲线下的面积,即AUC (Area Under ROC Curve),