背景

  我的朋友海伦,一直使用在线约会网站,寻找适合自己的约会对象。约会网站会推荐给她不同的人选,但是她没有从中找到喜欢的人。她总结了一下以前交往过的人有3种类型(不喜欢、魅力一般、极具魅力),但是她不知道网站推荐的人选究竟属于哪一类。
  需求:希望分类器能将这些推荐的人选,划分到确切的类别中。

解决思路

  knn

代码

  (1)收集数据
  (2)准备数据:filematrix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import numpy as np
#用Python处理文本文件非常容易
def filematrix(filename):
fr = open(filename) #打开文件
fr_lines = fr.readlines()
m = len(fr_lines) #文件文件包含多少行
X_raw = np.zeros((m,3)) #初始化特征矩阵
y_raw = [] #初始化标签列表
i = 0 #初始化指向特征矩阵的索引
mapdict = {'didntLike':1,'smallDoses':2,'largeDoses':3} #将标签的字符串转为数值
for line in fr_lines: #遍历每一行数据
line = line.strip() #删除最后的回车符
line_list = line.split('\t') #用\t分割该行数据
X_raw[i,:] = line_list[0:3] #前三个元素是特征
y_raw.append(mapdict[line_list[-1]]) #最后一个元素是标签
i += 1
return X_raw,y_raw #返回原始特征矩阵、标签

1
2
3
4
5
6
7
8
9
10
11
>> X_raw, y_raw = filematrix('datingTestSet.txt')
>> X_raw
array([[ 4.09200000e+04, 8.32697600e+00, 9.53952000e-01],
[ 1.44880000e+04, 7.15346900e+00, 1.67390400e+00],
[ 2.60520000e+04, 1.44187100e+00, 8.05124000e-01],
...,
[ 2.65750000e+04, 1.06501020e+01, 8.66627000e-01],
[ 4.81110000e+04, 9.13452800e+00, 7.28045000e-01],
[ 4.37570000e+04, 7.88260100e+00, 1.33244600e+00]])
>> y_raw[:20]
[3, 2, 1, 1, 1, 1, 3, 3, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3]

  (3)归一化:autoNorm

1
2
3
4
5
6
7
8
9
def autoNorm(X_raw):        #输入是一个矩阵,而不是向量
minvals = X_raw.min(0) #每列的最小值
maxvals = X_raw.max(0) #每列的最大值
rangevals = maxvals - minvals #每列的极差
X_norm = np.zeros(X_raw.shape) #初始化特征矩阵
m = X_raw.shape[0] #特征矩阵行数
X_norm = X_raw - np.tile(minvals,(m,1)) #(分子)每个元素减去对应列的最小值
X_norm = X_norm/np.tile(rangevals,(m,1)) #每个元素除以对应列的极差
return X_norm, rangevals, minvals #返回每一列的极差、最小值,是为了之后输入一个特征向量时做预测

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> X_norm, rangelist, minvals = autoNorm(X_raw)
>>> X_norm
array([[ 0.44832535, 0.39805139, 0.56233353],
[ 0.15873259, 0.34195467, 0.98724416],
[ 0.28542943, 0.06892523, 0.47449629],
...,
[ 0.29115949, 0.50910294, 0.51079493],
[ 0.52711097, 0.43665451, 0.4290048 ],
[ 0.47940793, 0.3768091 , 0.78571804]])
>> rangevals
array([ 9.12730000e+04, 2.09193490e+01, 1.69436100e+00])
>> minvals
array([ 0. , 0. , 0.001156])

  (4)knn算法:knnFunc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def knnFunc(x_test, X_norm, y_raw, k):
m = X_norm.shape[0] #有几个样本(行数)
X_diff = np.tile(x_test,(m,1)) - X_norm #每个训练数据点与新数据的差值
X_sqdiff = X_diff**2 #每个元素取平方
x_sqdiffsum = X_sqdiff.sum(axis=1) #对每一行的元素求和
x_distance = x_sqdiffsum**0.5 #每个元素取根号,得到距离向量

sorted_indice = x_distance.argsort() #升序排序,返回索引
classCount={} #空字典,用来放{'类别':出现次数}

for i in range(k): #提取前k个索引
votellabel = y_raw[sorted_indice[i]] #索引对应的类别
classCount[votellabel] = classCount.get(votellabel,0) + 1 #存在→加1,不存在→为1
sortedClassCount = sorted(classCount.items(), key=lambda x:x[1], reverse=True) #返回以tuple为元素的列表,按照tuple的第二个元素排序
return sortedClassCount[0][0]

  (5)测试模型误分类率:datingClassTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def datingClassTest():
train_test_ratio = 0.05
X_raw, y_raw = filematrix('datingTestSet.txt') #读取原始数据
X_norm ,_,_ = autoNorm(X_raw) #归一化
m = X_norm.shape[0] #特征矩阵行数
test_num = int(m*train_test_ratio) #测试数据的个数
X_train = X_norm[test_num:m,:]
y_train = y_raw[test_num:m]
errorCount = 0.0 #初始化误分类次数
for i in range(test_num): #遍历测试数据的索引
knn_result = knnFunc(X_norm[i,:], X_train, y_train, 3)
if (knn_result != y_raw[i]):
errorCount += 1.0
error_rate = errorCount/float(test_num) #计算误分类率
return error_rate

1
2
>> datingClassTest()
0.2

  (6)使用模型

1
2
3
4
5
6
7
8
9
10
11
12
def classfyPerson():
x1 = float(input('每年获得的飞行常客里程数:')) #手动输入x1、x2、x3的值
x2 = float(input('玩视频游戏所消耗时间百分比:'))
x3 = float(input('每周消费的冰淇淋公升数:'))

X_raw, y_raw = filematrix('datingTestSet.txt')
X_norm, rangevals, minvals = autoNorm(X_raw)
x_pred = np.array([x1,x2,x3])
x_pred = (x_pred - minvals)/rangevals
y_pred = knnFunc(x_pred, X_norm, y_raw, 3)
mlist = ['didntLike','smallDoses','largeDoses']
print('您可能会认为这个人:%s' % (mlist[y_pred-1]))

1
2
3
4
5
>> classfyPerson()
每年获得的飞行常客里程数:10000
玩视频游戏所消耗时间百分比:10
每周消费的冰淇淋公升数:0.5
您可能会认为这个人:smallDoses

【补充】代码中使用到的函数

(1)文件对象的三个“读”方法:read()、readline()、readlines()
  read()每次读取整个文件,通常用于将文件内容放到一个字符串变量中。这对于连续的面向行的处理是比不要的,而且如果文件大于可用内存,则不可能实现这种处理;
  readlines()也是一次读取整个文件,自动将文件内容分析成一个列表,列表的元素是每一行字符串(可以用for循环提取)

1
2
3
4
5
6
7
>> fr = open('datingTestSet.txt')
>> for line in fr.readlines():
line = line.strip() #去掉最后的回车
print(type(line),line)

<class 'str'> 40920 8.326976 0.953952 largeDoses
<class 'str'> 14488 7.153469 1.673904 smallDoses

  readline()每次只读取一行,只有遇到回车(\r)或者换行符(\n)才会返回结构,通常比readlines()慢得多,仅当没有足够内存可以一次读取整个文件时,才应该使用它

1
2
3
4
5
6
7
8
>> fr = open('datingTestSet.txt')
>> while True:
line = fr.readline()
line = line.strip()
print(type(line), line)

<class 'str'> 40920 8.326976 0.953952 largeDoses
<class 'str'> 14488 7.153469 1.673904 smallDoses

(2)np.tile(数组, 数组重复的次数)

1
2
3
4
5
6
7
8
9
10
11
12
>> np.tile([0,0],2)
array([0,0,0,0]) #在列方向上重复2次,还是一维
>> np.tile([0,0],[1,1]) #在行方向上重复1次,列方向上重复1次
array([[0,0]])
>> np.tile([0,0],[2,1]) #行2次,列1次
array([[0,0]
[0,0]])
>> np.tile([0,0],[1,2]) #行1次,列2次,变成二维
array([[0,0,0,0]])
>> np.tile([0,0],[2,3]) #行2次,列3次
array([[0,0,0,0,0,0]
[0,0,0,0,0,0]])

(3)numpy数组的sum方法

1
2
3
4
5
6
7
>> a = np.array([[1,2,3],[4,5,6]])
>> a.sum()
21
>> a.sum(axis=0) #每一列的元素相加
array([5, 7, 9])
>> a.sum(axis=1) #每一行的元素相加
array([ 6, 15])

(4)字典的get方法

1
2
3
4
5
6
7
>> a = {'A':1, 'B':6}
>> a.get('A',0)
1
>> a.get('B',0)
6
>> a.get('C',0)
0

(5)对字典排序的三种方法
  ①sorted(dict.items(), key=lambda x:x[1])
  ②import operator; sorted(dict.items(), keys=operator.itemgetter(1))
  ③sorted(zip(d.values(), d.keys())
注意:sorted是内建函数,它的可选参数①reverse,默认是False,升序排序;②key,为每个元素提取比较值的函数
   dict.items返回[(key,value),..]的迭代器,是无序的
   lambda x:x[1]取每个tuple的第二个元素

1
2
3
>> a = {'A':1, 'B':6}
>> b = sorted(a.items(),key=lambda x:x[1], reverse=True)
[('B', 6), ('A', 1)]

(6)matplotlib.pyplot.scatter
参数①xy 是输入数据,是(n,)的数组
  ②s 设置数据点的大小,是变量或(n,)的数组,默认为20
  ③c 颜色序列
  ④marker 数据点的形状,默认为’o’
  ⑤cmap Colormap实例,默认None
  ⑥norm 数据点的亮度0-1,float数据,默认None
  ⑦alpha 透明度0-1,默认None

1
2
3
4
5
6
7
8
9
10
11
>> import matplotlib.pyplot as plt
>> fig = plt.figure(figsize=(8,6)) #画板
>> ax = fig.add_subplot(111) #画纸,或者前两个合并fig,ax = plt.subplots(1,1,figsize=(8,6))
>> ax.scatter(一系列参数) #画散点图
>> plt.show()

#plt无法正常显示中文的解决方法
>> from matplotlib.font_manager import FontProperties #导入字体
>> font_set = FontProperties(fname=r'c:\windows\fonts\simsun.ttc',size=15) #字体实例
>> ax.set_ylabel(u'x轴标签',fontproperties = font_set) #散点图的字体设置
>> ax.legend(prop = font_set) #图例的字体设置

【补充】用sklearn实现knn算法,用学习曲线评估

sklearn.neighbors.KNeighborsClassifier(n_neighbors=3, weights=’uniform’, algorithm=’auto’, leaf_size=30, p=2, metric=’minkowski’, metric_params=None)
  ①n_neighbors k值
  ②weights 临近实例的权重,可以取三个值 ‘uniform’是默认→权重相同的情况,’distance’权重与距离成反比、自定义函数对权值进行设定
  ③algorithm 可以取4个值,’auto’、’ball_tree’、’kd_tree’、’brute’
  ④leaf_size ball_tree和kd_tree树的叶子数量
  ⑤p 距离度量的p值
  ⑥metric 距离度量的方法

1
2
3
4
5
6
7
8
from sklearn.neighbors import KNeighborsClassifier

#提取数据并归一化
X,y_train = file2matrix('datingTestSet2.txt')
X_train ,_ ,_= autoNorm(X)

#创建knn算法实例
knn = KNeighborsClassifier(n_neighbors=3)

sklearn.learning_curve.learning_curve(estimator, X, y, groups=None, train_sizes=array([0.1, 0.33, 0.55, 0.78, 1. ]), cv=’warn’, scoring=None, exploit_incremental_learning=False, n_jobs=None, pre_dispatch=’all’, verbose=0, shuffle=False, random_state=None, error_score=’raise-deprecating’)

  ①estimator 训练器的名称
  ②X 训练集特征
  ③y 训练集标签
  ④train_sizes 划分出的训练集,占总数据集的比例0-1,也可以输入绝对数量
  ⑤cv 是整型,交叉验证folds的个数,默认是3
  ⑥scoring 打分函数
  ⑦n_jobs 并行运行的作业数,默认是1
  ⑧random_state 整型→随机数生成器使用的种子

  有3个返回值:x轴刻度数组、训练集分数(n_ticks, n_cv_folds)、测试集分数(n_ticks, n_cv_folds)。刻度数可能是:不同的样本数、某一参数的不同值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sklearn.learning_curve import learning_curve
import matplotlib.pyplot as plt

train_sizes,train_scores,test_scores=learning_curve(estimator=knn,X = X_train, y = y_train, train_sizes = np.linspace(0.1,1.0,10), cv=10, n_jobs = 1)

train_mean = np.mean(train_scores, axis=1) #每行的平均值(同一样本量不同folds)
train_std = np.std(train_scores, axis=1)

test_mean = np.mean(test_scores, axis=1)
test_std = np.std(train_scores, axis=1)

param_range = np.linspace(0.1,1.0,10) #训练集的比重
plt.plot(train_sizes, train_mean, color='blue', marker='o', markersize=5,label='training accuracy')
plt.fill_between(train_sizes,train_mean+train_std, train_mean-train_std, alpha=0.15, color='blue') #填充两个数据集之间的区域

plt.plot(train_sizes, test_mean, color='green', marker='s',linestyle='--', markersize=5,label='test accuracy')
plt.fill_between(train_sizes,test_mean+test_std, test_mean-test_std, alpha=0.15, color='green')

plt.grid()
plt.ylim([0.8, 1.0])
plt.legend(loc='lower right')
plt.xlabel('Number os training samples')
plt.ylabel('Accuracy')

  学习曲线
  训练集和测试集的准确率收敛,并且都很高。