【数学基础】
- 欧氏距离:最常见的两点之间或多点之间的距离表示法,又称之为欧几里得度量,它定义于欧几里得空间中,如点 x = (x1,…,xn) 和 y = (y1,…,yn) 之间的距离为:
def euclidean(x, y):
return np.sqrt(np.sum((x - y)**2))
- 曼哈顿距离:我们可以定义曼哈顿距离的正式意义为L1-距离或城市区块距离,也就是在欧几里得空间的固定直角坐标系上两点所形成的线段对轴产生的投影的距离总和。例如在平面上,坐标(x1, y1)的点P1与坐标(x2, y2)的点P2的曼哈顿距离为:
def manhattan(x, y):
np.sum(np.abs(x - y))
- 标准化欧氏距离:标准化欧氏距离是针对简单欧氏距离的缺点而作的一种改进方案。标准欧氏距离的思路:既然数据各维分量的分布不一样,那先将各个分量都“标准化”到均值、方差相等。至于均值和方差标准化到多少,先复习点统计学知识。
假设样本集X的数学期望或均值(mean)为m,标准差(standard deviation,方差开根)为s,那么X的“标准化变量”X*表示为:(X-m)/s,而且标准化变量的数学期望为0,方差为1。 即样本集的标准化过程(standardization)用公式描述就是:
标准化后的值 = ( 标准化前的值 - 分量的均值 ) / 分量的标准差,经过简单的推导代入到上面的欧式距离公式即可。
def standardized_euclidean(x, y):
z = np.vstack([x, y]) # 拼接两个向量
var_value = np.var(z, axis=0, ddof=1) # 计算方差
return np.sqrt(((x - y)**2 / var_value).sum())
- 汉明距离:两个等长字符串s1与s2之间的汉明距离定义为将其中一个变为另外一个所需要作的最小替换次数。
例如字符串“1111”与“1001”之间的汉明距离为2。
应用:信息编码(为了增强容错性,应使得编码间的最小汉明距离尽可能大)。
def hamming(x, y):
return np.sum(x != y) / len(x)
- 夹角余弦:几何中夹角余弦可用来衡量两个向量方向的差异,机器学习中借用这一概念来衡量样本向量之间的差异。
在二维空间中向量A(x1,y1)与向量B(x2,y2)的夹角余弦公式:
两个n维样本点a(x11,x12,…,x1n)和b(x21,x22,…,x2n)的夹角余弦:
夹角余弦取值范围为[-1,1]。夹角余弦越大表示两个向量的夹角越小,夹角余弦越小表示两向量的夹角越大。
当两个向量的方向重合时夹角余弦取最大值1,当两个向量的方向完全相反夹角余弦取最小值-1。
def cos_sim(x, y):
x = np.mat(x)
y = np.mat(y)
#num = float(np.vstack([x, y]) * y.T)
num = np.dot(x, y)
# x=[1, 2], np.linalg.norm(x)=np.sqrt(1^2 + 2^2)
denom = np.linalg.norm(x) * np.linalg.norm(y)
cos = num / denom
sim = 0.5 + 0.5 * cos
return sim
- 杰卡德相似系数:两个集合A和B的交集元素在A,B的并集中所占的比例,称为两个集合的杰卡德相似系数,用符号J(A,B)表示。杰卡德相似系数是衡量两个集合的相似度一种指标。
与杰卡德相似系数相反的概念是杰卡德距离:
def jaccard(x, y):
#其中x和y是set集合,set([1, 2]) set([2, 3, 4]),带入本函数求得为0.25
return float(len(x & y) / len(x | y))
- 皮尔逊系数:在统计学中,皮尔逊积矩相关系数用于度量两个变量X和Y之间的相关(线性相关),其值介于-1与1之间。通常情况下通过以下取值范围判断变量的相关强度:
皮尔森相关系数等于两个变量的协方差除于两个变量的标准差
0.8-1.0 极强相关 0.6-0.8 强相关 0.4-0.6 中等程度相关 0.2-0.4 弱相关 0.0-0.2 极弱相关或无相关。
经过标准化后的欧氏距离和余弦相似度,等价于皮尔森相关系数。
def pearson(x, y):
return np.corrcoef(x, y)
【KNN通俗解释】
何谓K近邻算法(K-Nearest Neighbor algorithm),单从名字来讲,可以简单理解为:K个最近的邻居,当K=1时,算法就可以理解为最近邻算法,即寻找最近的那个邻居。
使用官方的话来说,给定一个训练数据集,对新的输入实例,在训练数据中找到与该实例最近邻的K个实例(就是上面说的K个邻居),这K个实例大概率属于同一类,那么就把该实例划分到这类中。
【KNN算法原理】
- 距离度量 – 特征向量空间表示;
- 倒排取top – 找最近邻居集合;
- 邻居投票 – 确定预测标签;
【KNN最近邻分类算法的过程】
- 计算测试样本和训练样本中每个样本点的距离(常见的距离度量有欧式距离,马氏距离等);
- 对上面所有的距离值进行排序;
- 选前k个最小距离的样本;
- 根据这k个样本的标签进行投票,得到最后的分类类别;
【KNN优缺点】
- 优点:
- 思想简单,理论成熟,既可以做分类也可以做回归;
- 可用于非线性分类;
- 训练时间复杂度为O(n);
- 准确度高,对数据没有假设;
- 缺点:
- 计算量太大;
- 对于样本分类不均衡的问题,会产生误判;
- 需要大量的内存;
- 输出的可解释性不强;
以上的1、3缺点解决方案:可采用离线的方式解决,比如先离线事先生成一个结果存放到redis中,然后供在线使用,每隔2h跑模型更新下redis缓存结果。
【K值选择】
- 如果选择较小的K值,就相当于用较小的领域中的训练实例进行预测,“学习”近似误差会减小,只有与输入实例较近或相似的训练实例才会对预测结果起作用,与此同时带来的问题是“学习”的估计误差会增大,换句话说,K值的减小就意味着整体模型变得复杂,容易发生过拟合;
- 如果选择较大的K值,就相当于用较大领域中的训练实例进行预测,其优点是可以减少学习的估计误差,但缺点是学习的近似误差会增大。这时候,与输入实例较远(不相似的)训练实例也会对预测器作用,使预测发生错误,且K值的增大就意味着整体的模型变得简单;
- K=N,则完全不足取,因为此时无论输入实例是什么,都只是简单的预测它属于在训练实例中最多的累,模型过于简单,忽略了训练实例中大量有用信息。
在实际应用中,K值一般取一个比较小的数值,例如采用交叉验证法(简单来说,就是一部分样本做训练集,一部分做测试集)来选择最优的K值;
【交叉验证】
交叉验证的目的实际上就是衡量分类器、回归器在未知样本(验证集样本)上面的误差,方法:
- 留一法:
将稳定的样本作为验证集,比如1000个样本,直接选300作为验证集,不过弊端是划分一次有可能不太准; - K折交叉验证-shuffle,比如:
原始样本:[1, 2, 3, 4, 5, 6, 7, 8, 9,10]
for i in n:
训练样本:[1, 2, 3, 4, 5, 6, 7,10]
测试样本:[8, 9]
其实就是在上面第一种方法中做多次。
比如n=5,当然每次验证集都可以拿其他样本,比如[8,9]/[1,2]/[3,4]/[5,6]/[7,8]/[9,10] ,每一次都能得到一个验证集的评估指标{1:78.0, 2:79.8, 3:80.1, 4:76.9, 5:76.0},最后取5次均值。通过交叉验证就可以取得一个合适的K值。
【实战手写数字识别】
数据集地址:
https://download.csdn.net/download/LWY_Xing/13215090
import numpy as np
from os import listdir
file_path = './handwritingClass/'
training_file_dir = file_path + 'trainingDigits/'
training_rows = len(listdir(training_file_dir))
#图片为32*32由0和1组成的数字
training_img_vect = np.zeros((training_rows, 32*32))
training_labels = []
m = 0
for file_name in listdir(training_file_dir):
#通过文件名或者真实的标签,0_23.txt 表示为0的数字图片
per_label = file_name.split('_')[0]
training_labels.append(per_label)
train_file = training_file_dir + file_name
fr = open(train_file)
#将数字图片的二进制数字放入到一个numpy数字中并铺平
per_img_vect = np.zeros((1, 32*32))
for i in range(32):
linestr = fr.readline()
for j in range(32):
per_img_vect[0, 32 * i + j] = int(linestr[j])
training_img_vect[m, :] = per_img_vect
m = m + 1
print(training_img_vect.shape)
print(len(training_labels))
#以下是处理测试集图片,方法跟上面大同小异
test_file_dir = file_path + 'testDigits/'
test_rows = len(listdir(test_file_dir))
test_img_vect = np.zeros((test_rows, 32*32))
test_labels = []
m = 0
for file_name in listdir(test_file_dir):
per_label = file_name.split('_')[0]
test_labels.append(per_label)
test_file = test_file_dir + file_name
fr = open(test_file)
per_img_vect = np.zeros((1, 32*32))
for i in range(32):
linestr = fr.readline()
for j in range(32):
per_img_vect[0, 32 * i + j] = int(linestr[j])
test_img_vect[m, :] = per_img_vect
m = m + 1
#遍历整个测试集,将每张测试图片与整个训练集计算欧式距离,并获取训练集中前k个与测试图片距离最小的label,
#然后从这个k中取label数据量最大的那个label,即为测试图片分类的label
errorCount = 0
for i in range(test_img_vect.shape[0]):
inputX = test_img_vect[i, :]
labelY = test_labels[i]
inputX = np.tile(inputX, (training_img_vect.shape[0], 1))
diffMat = inputX - training_img_vect
diffMat = diffMat ** 2
distances = diffMat.sum(axis=1) ** 0.5
#排序并获取对应的索引值
sortDistances = distances.argsort()
k = 5
labelCount = {}
for i in range(k):
index = sortDistances[i]
label = training_labels[index]
#print('neariest index = %d, label = %s' %(index, label))
labelCount[label] = labelCount.get(label, 0) + 1
#print(labelCount) # {'9': 3, '5': 2}
maxLabelCount = max(labelCount, key=labelCount.get)
#print('the img real label %s, predict label = %s' %(labelY, maxLabelCount))
if maxLabelCount != labelY:
errorCount = errorCount + 1
print('test img total = %d, error count = %d, error rate = %0.3f' %(test_rows, errorCount, round(errorCount / test_rows, 3)))
(1934, 1024)
1934
test img total = 946, error count = 17, error rate = 0.018
【sklearn手写数字识别】
import numpy as np
from os import listdir
from sklearn.neighbors import KNeighborsClassifier as kNN
'''
将单个灰度图像(32*32)转换为向量(1*1024)
file_name: 图像具体的路径文件名称
'''
def img2vector(file_name):
img_vector = np.zeros((1, 1024))
fr = open(file_name)
for i in range(32):
line_str = fr.readline()
for j in range(32):
img_vector[0, i * 32 + j] = int(line_str[j])
return img_vector
#img2vector(train_data_path + '/0_1.txt')
'''
收集数据集并转换为numpy矩阵(samples, 1024)
file_root_path: 数据集根目录
result: 返回样本空间以及对应标签
'''
def transformDatasets(file_root_path):
data_list = listdir(file_root_path)
file_size = len(data_list)
datasets = np.zeros((file_size, 1024))
labels = []
for i in range(file_size):
file_name = data_list[i]
label = file_name.split('_')[0]
datasets[i] = img2vector(file_root_path + '/' + file_name)
labels.append(label)
return datasets, labels
train_data_path = './handwritingClass/trainingDigits'
test_data_path = './handwritingClass/testDigits'
train_datasets, train_labels = transformDatasets(train_data_path)
test_datasets, test_labels = transformDatasets(test_data_path)
neigh = kNN(n_neighbors=3, algorithm='auto', weights='distance', n_jobs=1)
#训练模型
neigh.fit(train_datasets, train_labels)
print(neigh.score(test_datasets, test_labels))
#验证测试数据集
error_count = 0
test_data_list = listdir(test_data_path)
test_file_len = len(test_data_list)
for i in range(test_file_len):
file_name = test_data_list[i]
label = file_name.split('_')[0]
test_dataset = img2vector(test_data_path + '/' + file_name)
pred_label = neigh.predict(test_dataset)[0]
#print('真实标签: %s, 预测标签: %s' %(label, pred_label))
if(label != pred_label):
error_count += 1
print('模型预测总错误个数: %d, 错误率为: %.3f' %(error_count, float(error_count / test_file_len)))
0.987315010571
模型预测总错误个数: 12, 错误率为: 0.013
【能否使用朴素贝叶斯做房价预测】
朴素贝叶斯主要是针对label为离散型值做分类问题,但是房价预测的label为连续型值,为回归问题。
如果一定要使用朴素贝叶斯,可以把回归问题转换为分类问题,即:把回归数值分桶,做多分类,但是违背了概率中的独立性要求,
而且分桶相当于自动把精确标签近似化了,所以效果会比较差,一般不会这么使用。
【实际使用中的一些经验和要点】
- 不必使用距离作为最后的倒排算法,作为粗排序/粗召回;
- 全量计算->采样计算(模型加速手段);
- 归一化:数值维度拉取映射到可比较的空间;
- 使用计算复杂度更低的距离度量函数(比如杰卡德算法);
- 使用ANN(k个相似近邻)替代KNN;