K近邻算法(KNN,K-NearestNeighbor)是机器学习或数据分析中最基础、也是最简单的算法之一,这个算法的思路就如同它字面上的意思“K个最近的邻居”,想要得到某个样本的某个特征的值(一个样本通常有多个特征),就需要找到距离它最近的K个样本,然后根据这些样本的该特征的近似值作为它的特征值。
样本和特征:通常来讲,可以理解为一个表格数据中一行数据为一个样本,一列数据为这个样本的一个特征,就像数据库中的记录和字段的关系。
距离和K值:这个算法的关键点在于距离的计算方法和K值的选取,距离的计算方式可以根据实际情况自定义,比如使用两个样本的某个特征值的差值绝对值作为这两个样本之间的距离,也可以使用比较通用的欧式距离计算方式,或者直接使用某些库自带的距离计算方式,如scipy库中就有计算距离的方法“from scipy.spatial import distance”,这几种距离的计算方式在本文示例中都有讲解,可以参考下。关于K值的选取,通常不宜过大,K值太大时,准确率会随之降低,通常选择3-10就足够了。
优点和缺点:优点就是思路简单,易于实现,理解了这个算法后,可以不用复杂的公式也能计算出来。缺点是需要计算每个样本与自身之间的距离,当样本数量较大时,计算量也随之增大,而且当样本之间的特征不平衡时,得出的结果的偏差也会随之增大。
注:机器学习中会涉及许多数学中的概念,如果有不清楚的地方,可能是学过但忘了,也可能是以前就没接触过,可以再去复习一下,或者干脆就重新学习一下,本文就不再详细讲解了。
本文将根据一个示例来实现和讲解K近邻算法,示例的需求是这样的:我手中有一套房子需要出租,但是价格不知道定为多少是最合适的,现在需要参考其他房东的出租信息来制定我的出租价格。示例将分为以下几部分内容来讲解:
- KNN算法实现
- 模型评估
- 基于多变量KNN模型
KNN算法实现
-
数据准备
需要准备的数据为其他房东的出租数据,我们将会根据这些数据作为参考得出自己房子的合适出租价格,这里准备的少量数据只是为了演示用,实际上应该多准备一些数据,得出的价格才能更加精确。
import pandas as pd
# 假设这些是我们获取到的租房信息
rental_info = {
'url': ['https://www.airbnb.com/rooms/975833', 'https://www.airbnb.com/rooms/295345', 'https://www.airbnb.com/rooms/295346',
'https://www.airbnb.com/rooms/333613', 'https://www.airbnb.com/rooms/805961', 'https://www.airbnb.com/rooms/7087327',
'https://www.airbnb.com/rooms/8249488', 'https://www.airbnb.com/rooms/8409022', 'https://www.airbnb.com/rooms/8411173',
'https://www.airbnb.com/rooms/8634774', 'https://www.airbnb.com/rooms/8498095', 'https://www.airbnb.com/rooms/8513660',
'https://www.airbnb.com/rooms/8298145', 'https://www.airbnb.com/rooms/1745866', 'https://www.airbnb.com/rooms/7678268',
'https://www.airbnb.com/rooms/8457865', 'https://www.airbnb.com/rooms/6757134', 'https://www.airbnb.com/rooms/8479636',
'https://www.airbnb.com/rooms/2310297', 'https://www.airbnb.com/rooms/6556520', 'https://www.airbnb.com/rooms/8519782',
'https://www.airbnb.com/rooms/8606980', 'https://www.airbnb.com/rooms/8485995', 'https://www.airbnb.com/rooms/8607216',
'https://www.airbnb.com/rooms/8568945', 'https://www.airbnb.com/rooms/1026034', 'https://www.airbnb.com/rooms/2486785',
'https://www.airbnb.com/rooms/1822257', 'https://www.airbnb.com/rooms/5220279', 'https://www.airbnb.com/rooms/3419118'],
'name': ['Historic DC Condo-Walk to Capitol!', 'Spacious Capitol Hill Townhouse', 'Spacious/private room for single',
'A wonderful bedroom with library', 'Downtown Silver Spring', 'Exclusive Catamaran Houseboat',
'Cozy DC Condo, Close to Metro!', 'Warm and Cozy 1 Bedroom Apt', 'Private room for rent',
'Elite Room w/private bath Eden Park', 'Takoma Comfort, DC Convenience', 'Great Penthouse View! Metro in 7min',
'Sunny & Conveniently Located!', 'Beautiful Private High-Rise Apt', 'Renaissance Rm shared bath Eden PK',
'Cozy private second floor', 'Cheap room near Fort Totten Metro', 'Near Washington,DC',
'Sweet basement suite apartment', 'Etta Mae Inn B&B - The Kiera room', 'All the comfort you need.',
'Etta Mae Inn B&B - The Phoenix room', 'Sunny Cape Cod minutes from DC', 'CHIC DC URBAN RETREAT',
'Perfect studio in VERY central DC', 'Logan Circle Loft 1bedroom, 1.5bath', 'Updated 2BD Rowhome Steps to Metro',
'Cozy Apt in the Heart of Hipsterdom', 'Beautiful room in amazing location!', 'Clean Studio - Next to Conv Center!'],
'host_id': [15830506, 5338703, 1487418, 16970249, 30369828, 951119, 4628, 3671500, 5159038, 347309, 9188872, 21704152,
44519208, 3736766, 347309, 44659281, 11798122, 34290236, 4218349, 45276150, 44546458, 45276150, 2245859,
966914, 2070536, 12725500, 9540128, 22207701, 12772446, 4240274],
'accommodates': [4, 6, 1, 2, 4, 4, 4, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 2, 4, 2, 2, 2, 7, 1, 4, 4, 3, 4, 2, 2],
'bedrooms': [4, 6, 1, 2, 4, 4, 4, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 2, 4, 2, 2, 2, 7, 1, 4, 4, 3, 4, 2, 2],
'bathrooms': [1, 3, 2, 1, 1, 1, 2, 1, 1.5, 2, 1.5, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1.5, 1, 1, 1, 1],
'beds': [2, 3, 1, 1, 1, 4, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 3, 1, 2, 1, 2, 2, 1, 1],
'price': ['$160.00 ', '$350.00 ', '$50.00 ', '$95.00 ', '$50.00 ', '$99.00 ', '$100.00 ', '$100.00 ', '$38.00 ',
'$71.00 ', '$97.00 ', '$55.00 ', '$50.00 ', '$99.00 ', '$60.00 ', '$52.00 ', '$23.00 ', '$200.00 ',
'$40.00 ', '$135.00 ', '$100.00 ', '$225.00 ', '$129.00 ', '$149.00 ', '$150.00 ', '$175.00 ', '$239.00 ',
'$65.00 ', '$71.00 ', '$80.00 '],
'minimum_nights': [1, 2, 2, 1, 7, 1, 3, 1, 2, 2, 4, 3, 2, 7, 2, 1, 1, 1, 1, 1, 1, 1, 14, 3, 2, 1, 5, 2, 2, 1],
'maximum_nights': [1125, 30, 1125, 1125, 1125, 1125, 1125, 1125, 180, 365, 1125, 1125, 14, 1125, 1125, 1125,
1125, 1125, 1125, 1125, 1125, 1125, 1125, 31, 365, 1125, 1125, 1125, 14, 1125],
'number_of_reviews': [0, 65, 1, 0, 0, 0, 0, 0, 1, 4, 5, 1, 0, 0, 2, 1, 1, 0, 0, 0, 0, 0, 0, 46, 84, 25, 4, 2, 83, 19],
'cleaning_fee': ['$115.00 ', '$100.00 ', '$100.00', '$15.00 ', '$15.00 ', '$50.00 ', '$50.00 ', '$10.00 ',
'$10.00 ', '$10.00 ', '$50.00 ', '$20.00 ', '$20.00 ', '$125.00 ', '$10.00 ', '$5.00', '$5.00 ',
'$99.00 ', '$99.00 ', '$99.00 ', '$99.00 ', '$99.00 ', '$99.00 ', '$30.00 ', '$30.00 ', '$25.00 ',
'$50.00 ', '$12.00 ', '$12.00 ', '$12.00 '],
'zipcode': [20003, 20003, 20782, 20024, 20910, 20024, 20012, 20712, 20743, 20912, 20912, 20910, 20910, 20910,
20912, 20712, 20782, 20910, 20712, 20912, 20712, 20912, 20912, 20008, 20005, 20005, 20001, 20001, 20001, 20001]
}
# 如果数据是存放在类似csv格式的文本文件中,可以使用pd.read_csv('xxx.csv')来读取数据
rental_df = pd.DataFrame(rental_info)
# 选取我们需要参考的信息:accommodates(可以容纳的房客数量),bedrooms(卧室数量),bathrooms(卫生间数量),
# beds(床的数量),price(房租价格),minimum_nights(房客最少租了多少天),maximum_nights(房客最多租了多少天),
# number_of_reviews(评论数量)
key_info = ['accommodates', 'bedrooms', 'bathrooms', 'beds', 'price', 'minimum_nights', 'maximum_nights', 'number_of_reviews']
keyinfo_df = rental_df[key_info]
# 查看前五条数据
keyinfo_df.head()
-
距离计算
假设我们以卧室数量作为参考,我们自己的房子有4个卧室,那么我们的房子与其他房东的房子的距离可以使用欧式距离计算方式,由于我们只有卧室数量这个单一变量,所以就可以简单计算为两者数量的差的绝对值。欧式距离公式如下:
注:其中q1-qn为一条数据(样本)的n个特征信息,p1-pn为另一条数据(样本)的特征信息,由此公式便可同时根据多个变量(多个特征)计算出两个样本之间的距离。
import numpy as np
my_bedrooms = 4
# 新加一列用以存放其他的房子与我们自己房子的距离
keyinfo_df['distance'] = np.abs(keyinfo_df['accommodates'] - my_bedrooms)
# 统计这些距离值并排序,以便查看
print(keyinfo_df['distance'].value_counts().sort_index())
keyinfo_df.head()
可以看出,与我们房子的卧室数量相同的有9个。
-
选择K个近邻算出价格
我们这里指定K=5,即选择5个与我们距离最近的房子作为参考,并算出近似值作为我们出租房子的参考价格。
# 将价格列的字符串去掉$符并转换为float类型
keyinfo_df['price'] = keyinfo_df['price'].str.replace('$', '').astype(float)
# sample函数具有“洗牌”的功能,先打乱数据的顺序,再返回指定的数据量,frac=1表示返回100%的数据,random_state为随机数种子
keyinfo_df = keyinfo_df.sample(frac=1, random_state=0)
# 按照distance列排序
keyinfo_df = keyinfo_df.sort_values('distance')
# 这里近似值的算法我们采用求平均的方式,即选择与我们距离最近的5个房子的价格计算平均值
my_price = keyinfo_df['price'].iloc[:5].mean()
my_price
输出结果:105.8
结论:以卧室数量作为参考,我们可以将房子的出租价格定在105.8左右。
模型评估
模型即我们的这个算法,对于上面这种实现方式,到底可不可靠?在不同的数据中计算得出的结果的误差有多大?这就是模型评估需要做的事。
为了评估模型(算法),我们需要将数据打乱之后分为两组,一组作为训练集(数据量占比较大),一组作为测试集(数据量占比较小),训练集数据用来优化改进我们的算法模型,测试集用来验证我们的算法模型。
验证方法为通过计算我们测试集数据得出的结果与测试集中原本的实际数据之间的误差来验证算法模型的可靠性,误差越小,则越可靠,这里我们采用的误差计算方式为均方根(RMSE,Root Mean Squared Error),计算公式如下:
注:actual表示实际值,predicted表示预测出来的值,1-n表示不同的样本,最终的值表示通过多个样本计算出来总体误差值。
keyinfo_df = keyinfo_df.sample(frac=1, random_state=0) # 再次打乱数据
keyinfo_df.drop('distance', axis=1) # 删除distance列
train_df = keyinfo_df.copy().iloc[:20] # 选取前20条数据作为训练集
test_df = keyinfo_df.copy().iloc[20:] # 选取后10条数据作为测试集
def predict_price(feature_value, feature_name):
"""
根据特征值feature_value,在训练集中根据指定特征feature_name得出预测的价格
"""
temp_df = train_df
temp_df['distance'] = np.abs(temp_df[feature_name] - feature_value)
temp_df = temp_df.sort_values('distance')
my_price = temp_df['price'].iloc[:5].mean()
return my_price
# 以测试集中的每条数据的卧室数量作为“我们自己的房子的卧室数量”,去训练集中计算得出参考价格
test_df['my_price'] = test_df['accommodates'].apply(predict_price, feature_name='accommodates')
# 根据公式计算均方根误差
test_df['squared_price'] = (test_df['price'] - test_df['my_price']) ** 2
mean_value = test_df['squared_price'].mean()
rmse = mean_value ** (1/2)
rmse
输出结果:101.82695124572865
单个误差值看不出什么意义,需要多个特征之间误差值联合起来一起看才有意义,误差值更小的那种计算方式通常更为可靠。
# 根据多个特征分别计算这种算法模型的均方根误差
for feature in ['accommodates','bedrooms','bathrooms','number_of_reviews']:
test_df['my_price'] = test_df[feature].apply(predict_price, feature_name=feature)
test_df['squared_price'] = (test_df['price'] - test_df['my_price']) ** 2
mean_value = test_df['squared_price'].mean()
rmse = mean_value ** (1/2)
print('{}: {}'.format(feature, rmse))
accommodates: 101.82695124572865
bedrooms: 101.82695124572865
bathrooms: 99.81280478976633
number_of_reviews: 87.14700224333595
基于多变量KNN模型
基于多变量的KNN模型意思是同时参考多个特征来选取距离自己最近的K个样本,即需要同时参考多个特征来计算与自己的距离,这种情况通常不会再自己写计算距离的算法了,而是借助于其他已经实现好的库,比如scipy库中的distance模块“scipy.spatial.distance”或者sklearn库中的neighbors模块“sklearn.neighbors”。以下就来介绍一下如何使用这两个库来实现KNN算法。
- 数据准备:标准化和归一化
# 重新获取数据
key_info = ['accommodates', 'bedrooms', 'bathrooms', 'beds', 'price', 'minimum_nights', 'maximum_nights', 'number_of_reviews']
keyinfo_df = rental_df[key_info]
keyinfo_df['price'] = keyinfo_df['price'].str.replace('$', '').astype(float)
keyinfo_df.head()
from sklearn.preprocessing import StandardScaler
# 去除数据中的空值,示例中虽然没有空值,但实际操作中应该有这一步,至少要处理一下空值
keyinfo_df = keyinfo_df.dropna()
# 将数据归一化和标准化,这里采用的处理方式是StandardScaler中的fit_transform方法,即均值方差归一化
# 这种归一化方法会使得数据集的方差为1,均值为0,符合标准正态分布,公式为:X=(x-μ)/σ
# 可以发现处理后的数据它们之间的差值变小了,这样的话使用欧式距离计算的误差就会更小了
keyinfo_df[key_info] = StandardScaler().fit_transform(keyinfo_df[key_info])
# 这里只是另外起一个变量名称,表示它已经是标准化的数据了
normalized_df = keyinfo_df
normalized_df.head()
-
使用scipy.spatial.distance计算距离
scipy库中的distance模块有多种计算距离的方法,这里使用cdist()方法进行计算,这个方法默认也是采用欧氏距离的计算方式,当然也可以通过指定metric参数更换距离计算方式,具体支持的距离计算方式可以自行去查阅API文档,这里就不单独介绍了。
from scipy.spatial import distance
normal_train_df = normalized_df.copy().iloc[:20]
normal_test_df = normalized_df.copy().iloc[20:]
def predict_price_multi(feature_values, feature_names):
"""
根据多个特征值feature_values,在训练集中根据指定的多个特征feature_names得出预测的价格
"""
temp_df = normal_train_df
# distance.cdist用于计算两组数据之间的距离,可使用metric参数指定距离的计算方式,默认为euclidean(欧几里得距离,即欧式距离)
temp_df['distance'] = distance.cdist(temp_df[features], [feature_values[feature_names]])
temp_df = temp_df.sort_values('distance')
my_price = temp_df['price'].iloc[:5].mean()
return my_price
# 同时使用多个特征参与距离的计算
features = ['accommodates', 'bedrooms', 'bathrooms']
normal_test_df['my_price'] = normal_test_df[features].apply(predict_price_multi, feature_names=features, axis=1)
# 根据公式计算均方根误差
normal_test_df['squared_price'] = (normal_test_df['price'] - normal_test_df['my_price']) ** 2
mean_value = normal_test_df['squared_price'].mean()
rmse = mean_value ** (1/2)
rmse
输出结果:0.925497918931224
注:可以看到,通过数据的标准化和归一化,最后计算出来的均方根误差值完全不一样了,小了很多,这样也能更符合我们对于误差的比较和判断方式了。
-
使用sklearn.neighbors实现KNN
sklearn库是Python中机器学习的库,大多机器学习的操作和算法在sklearn中都能找到,当然也包括KNN算法,而且都已经封装好了,我们只需要拿来用就行了。
from sklearn.neighbors import KNeighborsRegressor
from sklearn.metrics import mean_squared_error
features = ['accommodates', 'bedrooms', 'bathrooms']
# 创建一个KNN回归器,默认k=5,即参数n_neighbors=5
knr = KNeighborsRegressor()
# 传入训练集和目标值
knr.fit(normal_train_df[features], normal_train_df['price'])
# 根据提供的测试集算出目标值
feature_predict = knr.predict(normal_test_df[features])
# 计算均方根误差
mse = mean_squared_error(normal_test_df['price'], feature_predict)
rmse = mse ** (1/2)
rmse
输出结果:1.139347164704861