Google Machine Learning Crash Course
简介
视角:从数学科学转移到自然科学。观察、做实验、使用数据,而不是使用逻辑去解决问题。像科学家一样思考。
(监督式)机器学习:组合输入信息,对未见过的数据做出预测。
其实也就是通过研究数据,得到模型。比如二元线性回归,那条直线,就是模型。
框架处理
- 标签:lable,要预测的真实事物,线性回归中的y;
- 特征:feature,描述数据的输入变量,线性回归中的x;
特征必须是可观察可量化的。比如像客户推荐鞋子,“用户对鞋有兴趣”不是标签,因为无法量化,但是“用户查看鞋子描述”是感兴趣的一种体现,可作为特征。同理,“鞋的美观程度”也不是特征,“美观”不是客观的信息,也无法界定,但是鞋码、材质等是可以作为特征的。
回归 vs. 分类
- 回归模型预测连续值,比如房价,结果为无限个;
- 分类模型预测离散值,比如是否为垃圾邮件,是猫、狗还是老鼠。结果为有限个,比如布尔型或枚举型;
深入了解
平方损失(L2 Loss)
平方和。有时候会取平方和的平均值,均方误差(MSE,Mean Squared Error),或者开二次方,得到均方根误差,RMSE,Root Mean Squared Error。RMSE 的一个很好的特性是,它可以在与原目标相同的规模(数值具有了可比较性)下解读。
虽然 MSE 常用于机器学习,但它既不是唯一实用的损失函数,也不是适用于所有情形的最佳损失函数。
降低损失
梯度下降
对于一个特征(一元)的线性回归函数,L2损失函数是关于w1的二次函数,不同的w值对应了不同的损失。损失最小就是w的倒数为零。当然一元这么说很简单,但是多元的时候,可以不算,直接拿不同的w去试,按照负梯度(让误差变小的方向,正梯度方向会让误差更大)的方向走。
梯度:各个feature(比如三维空间就是x轴y轴方向)偏导数的矢量和。
关于起点:对于碗状的,起点并不重要,但是在神经网络中,更像是蛋托,所以起点不一样,梯度下降到的最小loss就不一样,起点很重要。
批量
理论上来讲,对于二维的一元线性回归: \(y = wx + b,\) \(loss = (wx1+b-y1)^2 + (wx2+b-y2)^2 + ... + (wxn+b-yn)^2\)
机器学习求w的步骤为(如果多元,就是求w0,w1,…wN):
- 随便选个w0;
- 带入全数据集,算出loss函数在w0的梯度,算是一步;
- 梯度×步长,假设为△x,再走一步,算出在(w0+△x)的梯度;
- 知道达到了指定步数,停止;
但是如果数据集太大,第二步算梯度计算量太大。而且,一般数据集包含大量特征(w0,w1,…wN),如果再加上特征组合,计算量更是上天。根据经验:
通过从我们的数据集中随机选择样本,我们可以通过小得多的数据集估算(尽管过程非常杂乱)出较大的平均值。 随机梯度下降法 (SGD) 将这种想法运用到极致,它每次迭代只使用一个样本(批量大小为 1)。如果进行足够的迭代,SGD 也可以发挥作用,但过程会非常杂乱。“随机”这一术语表示构成各个批量的一个样本都是随机选择的。
- 随机梯度下降法 stochastic gradient descent, SGD;
- 实操时,可以折中一下,选择一小撮数据,比如10-1000个点,计算梯度,被称为小批量梯度下降法 mini-batch gradient descent,小批量SGD。
1
2
3
stochastic UK: [stɒ'kæstɪk], US: [stə'kæstɪk]
Word Explanation:
* adj. [数] 随机的;猜测的
学习速率(步长)
梯度下降法,下一个点的位置是:梯度×步长。所以梯度大的地方移动的就快,梯度小的地方移动的慢。移动距离并非一直一样,这就导致步长太大会导致下一个点在碗的底部来回任意弹跳。
而且,如果步长过大,移动到的曲线的另一半,梯度更大,那么乘以步长后往反方向移动的距离更大,将会离最小值越来越远……
TensorFlow
pandas
DataFrame和Series,表和列
创建Series
1
2
city_names = pd.Series(['San Francisco', 'San Jose', 'Sacramento'])
population = pd.Series([852469, 1015785, 485199])
创建DataFrame
如果Series不一样长,会使用NA/NaN填充
1
cities = pd.DataFrame({ 'City name': city_names, 'Population': population })
或者:
1
california_housing_dataframe = pd.read_csv("https://download.mlcc.google.cn/mledu-datasets/california_housing_train.csv", sep=",")
访问DataFrame
概览:
1
2
3
california_housing_dataframe.describe()
california_housing_dataframe.head()
california_housing_dataframe.hist('housing_median_age')
访问列,类型为Series:
1
cities['City name']
单独列的某行数据,类型为该数据类型,比如str:
1
2
3
4
# <class 'str'>
# 'San Jose'
print(type(cities['City name'][1]))
cities['City name'][1]
单独列的某些行,又构成了子列,所以类型为Series:
1
2
3
4
5
# <class 'pandas.core.series.Series'>
# 1 San Jose
# Name: City name, dtype: object
print(type(cities['City name'][1:2]))
cities['City name'][1:2]
访问行(前两行,不含第三行),类型依然为DataFrame:
1
cities[0:2]
操纵数据
增加列:
1
cities['Area square miles'] = pd.Series([46.87, 176.53, 97.92])
列转换:转换为true/false
1
population.apply(lambda val: val > 1000000)
修改列数据:
1
california_housing_dataframe["median_house_value"] /= 1000.0
index
Series和DataFrame的索引在创建好之后就是固定的:
1
2
3
4
# RangeIndex(start=0, stop=3, step=1)
# pandas.core.indexes.range.RangeIndex
city_names.index
type(city_names.index)
调整index顺序就是调整数据行的顺序:
1
2
3
4
5
# City name Population
# 2 Sacramento 485199
# 0 San Francisco 852469
# 1 San Jose 1015785
cities.reindex([2, 0, 1])
所以可以通过reindex加上随机函数(Numpy的random.permutation)将数据重排序:
1
cities.reindex(np.random.permutation(cities.index))
允许使用不存在的索引:
1
2
3
4
5
6
# City name Population
# 0 San Francisco 852469.0
# 4 NaN NaN
# 5 NaN NaN
# 2 Sacramento 485199.0
cities.reindex([0, 4, 5, 2])
TensorFlow API
简单线性回归使用Estimator的LinearRegressor。
大致划分为三步:创建模型,训练,预测:
1
2
3
4
5
6
7
8
9
10
import tensorflow as tf
# Set up a linear classifier.
classifier = tf.estimator.LinearClassifier()
# Train the model on some example data.
classifier.train(input_fn=train_input_fn, steps=2000)
# Use it to predict.
predictions = classifier.predict(input_fn=predict_input_fn)
不过在创建的时候,LinearRegressor需要指定比如GradientDescentOptimizer,指定学习速率learning_rate(步长)。同时定义特征列。(并不是传入特征列,传特征列在训练时传入)
1
2
3
4
5
6
7
8
9
10
11
# Use gradient descent as the optimizer for training the model.
my_optimizer=tf.train.GradientDescentOptimizer(learning_rate=0.0000001)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)
# Configure the linear regression model with our feature columns and optimizer.
# Set a learning rate of 0.0000001 for Gradient Descent.
feature_columns = [tf.feature_column.numeric_column("total_rooms")]
linear_regressor = tf.estimator.LinearRegressor(
feature_columns=feature_columns,
optimizer=my_optimizer
)
在训练的时候传入输入函数和步数steps,输入函数指定了feature和label:
1
2
3
4
linear_regressor.train(
input_fn = lambda:my_input_fn(my_feature, targets),
steps=100
)
最后评估预测模型,只需要给定预测输入函数,该函数指定了输入feature,但是这个样例也传了输出label的数据,按理说不该传的啊,难道是为了公用函数,实际只传没用???TODO:
1
predictions = linear_regressor.predict(input_fn=prediction_input_fn)
训练建议
- 训练误差应该稳步减小,刚开始是急剧减小,最终应随着训练收敛达到平稳状态;
- 如果还没有收敛趋势,继续运行更久(增加steps,步数,not步长);
- 如果走的太慢,建议增大步长(学习速率),但是太大时误差会出现不稳定(来回跳跃);
- 一开始推荐小速率,多走几步;
- 批量大小过小也会导致不稳定情况。不妨先尝试 100 或 1000 等较大的值,然后逐渐减小值的大小,直到出现性能降低的情况。
合成特征synthetic和离群值outlier
比如构造人均房屋数量:
1
2
3
california_housing_dataframe["rooms_per_person"] = (
california_housing_dataframe["total_rooms"] / california_housing_dataframe["population"]
)
通过分布直方图可以看出数据分布:
1
california_housing_dataframe["rooms_per_person"].hist()
17000个数据有接近17000个在5以内,其他没多少数据,最夸张能达到40+(贫富啊,资本主义啊……)
截取一下:
1
2
3
california_housing_dataframe["rooms_per_person"] = (
california_housing_dataframe["rooms_per_person"]).apply(lambda x: min(x, 5)
)
拿着去掉(modify)离群值的数据去训练模型,batch、learning_rate和steps不变的情况下,loss确实小了。
- steps:训练迭代总次数,每一步计算一批样本的损失,得出一次权重;
- batch size:单步的样本数量(随机选择);比如SGD的batch_size=1;
所以总训练样本数 = batch_size * steps
泛化 Generalization
泛化是指模型很好地拟合以前未见过的新数据(从用于创建该模型的同一分布中抽取)的能力。
过拟合
如果某个模型尝试紧密拟合训练数据,基本就不能很好地泛化到新数据,就会发生过拟合。过拟合一般都是模型超出了所需要的复杂程度所导致的。比如,训练集中可能有噪点,过于复杂的特征组合会与训练数据中的噪点拟合。 所以虽然对于当前数据集而言,loss很小,但是当新的数据到来的时候,反而可能和模型预测的不符,loss大增。
因为你不知道产生数据的机制是怎样的,只是根据当前显示出的标签和特征去预测。所以如果太贴合当前数据集,新产生的数据很可能不在模型的预测划分之内。过拟合了。
机器学习的基本冲突是适当拟合我们的数据,但也要尽可能简单地拟合数据。
奥卡姆剃刀定律: 机器学习模型越简单,良好的实证结果就越有可能不仅仅基于样本的特性。
评判模型的泛化能力:训练集 测试集
- 训练集 - 用于训练模型的子集。
- 测试集 - 用于测试模型的子集。
样本需要满足:
- 独立同分布,iid,independent identical distribution;
- 分布平稳,不随时间等变化;
- 从同一分布数据中抽取样本;
stationarity UK: [,steiʃən’ærəti], US: [,steiʃən’ærəti] Word Explanation: * n. 平稳性;稳定性;固定性
一般来说,在测试集上表现是否良好是衡量能否在新数据上表现良好的有用指标,前提是:
- 测试集足够大。
- 您不会反复使用相同的测试集来作假。
验证集 validation data
训练完毕,去测试集测试一发,发现不是很好,继续调整一些参数,继续训练,再测试……迭代多了,其实测试集也变成我们的训练集了,过拟合测试集了。那这模型效果肯定也不好。
所以不如将数据划分为训练集、验证集、测试集。(其实相当于把训练集拆分成两块了)。我们训练,然后再验证集上验证,再训练,直到效果不错,当且仅当此时,在测试集测试一次。
如果测试集效果不错,那说明模型不错。如果测试集效果不好,说明我们过拟合验证集了(也可以说是过拟合训练集了吧)。这时候感觉应该重建模型,重新调参。
有点儿像:开发+自测,可以了再提测。如果开发完就提测,发现不过再根据测试样例改代码,很可能最终只通过(应付过)了测试样例,但是实际上还是有bug的!这道理真的是想通的啊!
测试集(测试样例)越少暴露越好,这样过了测试更能证明模型(代码)的正确性。
特征表示 Representation
传统编程的关注点是代码。在机器学习项目中,关注点变成了特征表示。也就是说,开发者通过添加和改善特征来调整模型。
大概75%的时间在从数据中抽取特征。
许多机器学习模型都必须将特征表示为实数向量,因为特征值必须与模型权重相乘。
- 映射数值:整数和浮点数据不需要特殊编码,因为它们可以与数字权重相乘;
- 映射分类值:独热编码/多热编码。分类特征具有离散值,比如街区;
- 分箱:比如维度和房价,不能维度直接乘,将维度拆分为一个个分类值,然后用独热编码。可以按几度一箱,也可以按中位数分箱,能确保每个箱里数据个数相同,不需要担心离群值问题;
- 缩放特征值:如果一个特征是-1到1,一个特征5000到100000,则会影响模型。考虑把后者缩放。没必要进行完全相同的缩放(毕竟string转数字根本就没有什么比例之类的依据),可以将min、max线性映射到-1到1,或者是用Z得分等。缩放对单特征模型没啥意义;
- 处理离群值outlier,比如用log处理一下数据;
独热编码 one hot code 多热编码
二元的多值向量,符合为1,不符合为0。量太多就用稀疏表示法。比如有五个手指头,将手指头特征抽取为一个独热五元素特征向量,大拇指表示为[1, 0, 0, 0, 0]等。
如果向量里不止有一个1,就是多热编码。比如一个房子在多条街道上。
一些数字数据,比如邮政编码,不希望与权重相乘,也应该像String一样使用独热编码。
良好的特征
很少使用的离散值不应该作为特征
良好的特征值应该在数据集中出现大约5次以上。这样才能学习特征与标签的关联性。比如在一份房子的价格表中,拿houseId去作为特征没啥意义,因为一个id只出现一次。但是在广告数据中,deviceId算是有意义的,出现多次,此时bidId没啥意义。
特征含义要清晰
比如房子寿命用年而不是秒。这样清晰易懂,可以及时检查非法值。
实际数据不要掺入特殊值
例如售卖年限为正整数,不售卖用-1表示,-1就不能作为特征值直接用。因为-1是自定义的含义,所以可以用两个特征表示,一个int代表售卖年限,一个bool代表是否售卖。
考虑上游数据的不稳定性
比如cityId改了……凉凉……训练个鸡儿……
可视化
- histogram
- scatter plot
- rank metrics
可以确保数据正常,比较容易分析筛选数据。
特征组合
(感觉就是调节变量……)
非线性问题可以通过特征组合转换为线性问题。线性模型结合随机梯度下降法可以很容易地扩展到大规模数据集。
线性算法可以算出组合向量的权重,就像算出x1和x2的权重一样。换言之,虽然x1*x2表示非线性信息,但您不需要改变线性模型的训练方式来确定其系数的值。
组合独热矢量
机器学习模型经常组合独热矢量,比如经度bin5个和维度bin5个,一组合就是25个元素的特征矢量,标志某一个经纬度交叉的小格格区域的特征。
Regularization for Simplicity 正则化:为了简化模型
拟合训练集太紧密必然会连噪声一起拟合,导致过拟合,泛化效果不好。所以要降低模型复杂度,这被称为正则化。
即,不能仅以loss作为衡量模型的标准,而是以最小化(损失+复杂度)为目标,这称为结构风险最小化: $\text{minimize(Loss(Data|Model))}:损失项+正则化项
一种方法:将模型复杂度定义为权重的函数,权重绝对值越大,模型复杂度越大。所以自然想到平方和,定义L2正则化项(平方和xxx就是L2xxx):
$L_2\text{ regularization term} = ||\boldsymbol w||_2^2 = {w_1^2 + w_2^2 + ... + w_n^2}$
在这个公式中,接近于0的权重对模型复杂度几乎没有影响,而离群值权重则可能会产生巨大的影响。即,系数太大的项会让整个L2正则化项变大。
另外可以给正则化项增加lambda系数,称之为正则化率,用来调整正则化项所占的比重:
$\text{minimize(Loss(Data|Model)} + \lambda \text{ complexity(Model))}$
(这个系数跟调价ocpc的那个系数不谋而合,都是为了修改比重)
L2 正则化不会倾向于使权重正好为0。L2正则化降低较大权重的程度高于降低较小权重的程度。随着权重越来越接近于 0.0,L2 将权重“推”向 0.0 的力度越来越弱。
逻辑回归
逻辑回归和线性回归之间的主要区别在于它们的目标和应用领域。
线性回归是一种用于建立输入变量(自变量)与连续输出变量(因变量)之间关系的统计模型。它的目标是通过拟合一条直线(或超平面)来预测连续数值的输出。线性回归适用于回归问题,如预测房价、销售额等连续数值的预测。
逻辑回归是一种用于建立输入变量与离散输出变量之间关系的统计模型。它的目标是通过拟合一个S形曲线(sigmoid函数)来预测离散的输出,通常是二分类问题。逻辑回归适用于分类问题,如预测患病与否、垃圾邮件识别等。
因此,线性回归和逻辑回归在目标和应用领域上存在明显的差异。线性回归用于预测连续数值的输出,而逻辑回归用于预测离散的分类结果。
逻辑回归不是回归
既然回归是预测连续数值而不是分类,那逻辑回归同属于回归,为什么主要用于分类?
尽管名字中包含”回归”一词,逻辑回归实际上是一种用于解决分类问题的算法,而不是回归问题。
逻辑回归之所以主要用于分类,是因为它的输出是一个介于0和1之间的概率值,代表样本属于某个类别的概率。通常可以通过设置一个阈值,如0.5,将概率值大于阈值的样本划分为一类,小于阈值的样本划分为另一类。这样就可以实现二分类任务。
逻辑回归在处理多分类问题时,可以使用一对多(One-vs-Rest)或一对一(One-vs-One)的策略进行拓展。一对多策略将每个类别与其他所有类别组合,构建多个二分类模型。一对一策略在每两个类别之间构建一个二分类模型。通过这些策略,逻辑回归可以用于解决多分类问题。
尽管逻辑回归在分类问题中应用广泛且表现良好,但它仍然是一种基于线性模型的方法,对于复杂的非线性关系可能表现不佳。在某些情况下,其他分类算法如决策树、支持向量机、神经网络等可能更适合处理特定的分类问题。
既然如此,逻辑回归和线性回归的公共点是否在于都是“线性”,而非都是“回归”? 是的,逻辑回归和线性回归的共同点主要在于它们都是基于线性模型的算法,而不是仅仅因为它们都属于回归问题。
线性回归和逻辑回归都建立在线性模型的基础上。线性回归通过拟合一条直线或超平面来建立输入特征与连续数值输出之间的线性关系。逻辑回归也基于线性模型,但它通过应用逻辑函数(如sigmoid函数)将线性输出映射到一个介于0和1之间的概率值,用于分类问题。
因此,可以说逻辑回归和线性回归的共同点在于它们都是基于线性模型的算法,尽管应用的问题和输出类型不同。
线性分类?
那逻辑回归的名字是否有误?感觉应该叫“线性分类”?
逻辑回归的名字确实可以引起一些混淆,因为它更适合用于分类问题而非回归问题。这是由于历史原因而形成的术语,可能与它最初被提出时的背景和应用有关。
逻辑回归的名字源自于它使用了逻辑函数(如sigmoid函数)来进行分类。逻辑函数的输出范围介于0和1之间,可以被视为概率值,因此逻辑回归常被用于二分类问题。
虽然逻辑回归名字中的”回归”一词可能会引起误导,但在机器学习领域,逻辑回归已经成为一种常见的分类算法,并且被广泛使用。实际上,线性分类这个术语更适用于一些其他基于线性模型的分类算法,如线性支持向量机(Linear SVM)。
因此,尽管逻辑回归的名字可能会引起一些困惑,但在机器学习领域,它已经成为了指代特定算法的约定俗成的术语。
你觉得从功能来看,它是否应该叫“线性分类”?
从功能的角度来看,确实可以认为逻辑回归更适合被称为”线性分类”,因为它在本质上是基于线性模型进行分类的。
逻辑回归基于输入特征的线性组合,通过应用逻辑函数将线性输出映射到0和1之间的概率值,从而实现分类。虽然逻辑回归使用了”回归”这个术语,但它的输出并不是连续数值,而是代表样本属于某个类别的概率。
因此,“线性分类”这个术语更准确地描述了逻辑回归的主要功能,即基于线性模型进行分类。然而,在机器学习领域,逻辑回归已经被广泛接受并成为常用的术语,因此在实践中继续称之为”逻辑回归”是比较常见和普遍的做法。
在机器学习中,“回归”一词通常用于指代预测连续数值的问题,而逻辑回归主要用于分类问题。这种命名可能源于逻辑回归算法最初发展时的历史原因。
S函数
模型返回的是一个值,可以将其通过逻辑回归输出为概率。使用的是S型函数:
$y = \frac{1}{1 + e^{-z}}$
将模型b + w1x1 + w2x2 + … wNxN
替换z变量即可。
同时,对s函数取反函数得到:
$z = log(\frac{y}{1-y})$
所以z也叫对数几率,如果label 1 为点击,则z就是log(点击/不点击)的概率。(但是对数几率,意义不明显啊,所以应该意义不大……)
损失函数
对数损失函数: $Log Loss = \sum_{(x,y)\in D} -ylog(y') - (1 - y)log(1 - y')$
分类 Classification
分类问题是否就是逻辑回归?
不完全正确。逻辑回归是一种常用的算法,用于解决二分类问题。但是,分类问题并不仅限于逻辑回归。
分类问题是机器学习中的一类典型问题,目标是将输入数据分为不同的类别或标签。除了逻辑回归之外,还有许多其他算法可以用于解决分类问题,例如决策树、随机森林、支持向量机、朴素贝叶斯、神经网络等。
选择适当的分类算法取决于数据集的性质、特征的类型、问题的复杂性以及性能要求等因素。每种算法都有其自身的优势和适用范围。
因此,尽管逻辑回归是一种常见的用于解决二分类问题的算法,但在面对分类问题时,还有其他算法可以考虑,以选择最合适的模型。
阈值
逻辑回归返回概率,如果要映射为二值(比如广告是否点击),需要设定阈值。超过为1,否则为0.
正类别、负类别 vs. 真假
比如点击广告为正类别(Positive),不点击为负类别(Negative),点击发生为真,点击没发生为假,则:
- 真正例TP:预测点击,且真点了;
- 假正例FP:预测点击,但没点;
- 假负例FN:预测不点,但点了;
- 真负例TN:预测不点,且真不点;
header 1 | header 2 |
---|---|
TP:1 | FP: 1 |
FN: 8 | TN: 90 |
准确率Accuracy、精确率Precision、召回率Recall
- 准确率:预测对的/总的,即
(TP+TN)/(TP+TN+FP+FN)
,但意义不大,如果ctr模型一直预测不点,则准确率能达到99%+,但是这没什么意义; - 精确率:在被识别为正例的样本中,确实为正例的比例,真/总。即
TP/(TP+FP)
。例如预测点击的事件中,真点了占总预测点击的比例,如果预测十个点击,九个发生了,精确率是90%; - 召回率:在所有识别的样本中,被正确识别为正例的比例,真/总。即
TP/(TP+FN)
。例如预测5次点击,5次不点,实际4次点击,4次不点,则召回率为4/(4+4)=50%(精确率80%);
精确率高不代表模型好,比如只在100%确定点击的时候才预测会被点击,虽然精确率很高,但是漏过了很多点击;这种情况下,很多预测的“不点”都预测错了,所以召回率很低。精确率反映的是真正例:我说点,它就点。会让保守的人钻空子,保守的人只在100%把握的时候说点,所以精确率会很高。
如果阈值设的很低,全部预测为“点”,则假负例不存在,即“预测不点,但点了”这种情况不存在(废话,因为根本没预测不点),召回率为100%,但是精确率就会很低。召回率考察的是有真正例(分子要大),且没有假负例(分母要小):我说不点,还真就不点。会让激进的人钻空子,激进的人全都预测为点,就不会存在假负例。与此同时只要有一两个真正例,召回率就会很高。
模型必须完全预测正确的时候,精确率(没有假正例)和召回率(没有假负例)才会均为100%.
精确率和召回率此消彼长,因为在无法100%正确预测的情况下,越保守精确率越高,越激进召回率越高。
ROC
TBD