目录
1.K邻近算法简单介绍
2.knn算法原理
3.基于knn的邮件分类实例
1.K邻近算法简单介绍
K近邻算法(K-Nearest Neighbors,简称KNN)是一种用于分类和回归的统计方法。KNN 可以说是最简单的分类算法之一,同时,它也是最常用的分类算法之一。
注意:KNN 算法是有监督学习中的分类算法,它看起来和另一个机器学习算法 K-means 有点像(K-means 是无监督学习算法),但却是有本质区别的。
2.knn算法的原理
k-近邻算法(k-Nearest Neighbour algorithm)的工作原理:给定一个已知标签类别的训练数据集,输入没有标签的新数据后,在训练数据集中找到与新数据最邻近的 k 个实例,如果这 k 个实例的多数属于某个类别,那么新数据就属于这个类别。即由那些离新数据最近的 k 个实例来投票决定新数据归为哪一类。
最邻近分类算法是数据挖掘分类(classification)技术中最简单的算法之一,其指导思想是”近朱者赤,近墨者黑“,即由你的邻居来推断出你的类别。
该算法一般流程为:
-
对未知类别属性的数据集中的每个点依次执行一下操作:
- (1)计算已知类别数据集中的点与当前点之间的距离
- (2)按照距离递增次序排序
- (3)选取与当前点距离最小的k个点
KNN三要素
- 对于K值的选择,一般根据样本分布选择一个较小的值,然后通过交叉验证来选择一个比较合适的最终值;
- 当选择比较小的K值的时候,表示使用较小领域中的样本进行预测,训练误差会减小,但是会导致模型变得复杂,容易导致过拟合;
- 当选择较大的K值的时候,表示使用较大领域中的样本进行预测,训练误差会增大,同时会使模型变得简单,容易导致欠拟合;
- 一般使用欧几里德距离
- 本篇博客用到的是曼哈顿距离公式
-
曼哈顿距离中的距离计算:(本篇博客实例用到的距离计算方法)
曼哈顿距离中的距离计算公式比欧氏距离的计算公式看起来简洁很多,只需要把两个点坐标的 x 坐标相减取绝对值,y 坐标相减取绝对值,再加和。
从公式定义上看,曼哈顿距离一定是一个非负数,距离最小的情况就是两个点重合,距离为 0,这一点和欧氏距离一样。曼哈顿距离和欧氏距离的意义相近,也是为了描述两个点之间的距离,不同的是曼哈顿距离只需要做加减法,这使得计算机在大量的计算过程中代价更低,而且会消除在开平方过程中取近似值而带来的误差。欧氏距离公式:
d=∑mi=1(xi−yi)2−−−−−−−−−−−−√d = \sqrt{\sum_{i=1}^m(x_i - y_i)^2}d=i=1∑m(xi−yi)2
例如:求点(1,0,0,1)(1,0,0,1)(1,0,0,1)和(7,6,9,4)(7,6,9,4)(7,6,9,4)之间的距离:
(7−1)2+(6−0)2+(9−0)2+(4−1)2−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−√\sqrt{(7-1)^2+(6-0)^2+(9-0)^2+(4-1)^2}(7−1)2+(6−0)2+(9−0)2+(4−1)2
KNNKNNKNN在做回归和分类的主要区别在于最后做预测时的决策方式不同:
(1)分类预测规则:一般采用多数表决法或者加权多数表决法
1. 多数表决法:
- 每个邻近样本的权重是一样的,也就是说最终预测的结果为出现类别最多的那个类;
- 如图,待预测样本被预测为红色圆
2. 加权多数表决法:
- 每个邻近样本的权重是不一样的,一般情况下采用权重和距离成反比的方式来计算,也就是说最终预测结果是出现权重最大的那个类别;
- 如图,红色圆到待预测样本的距离为3,蓝色方块到待预测样本的距离为2,权重与距离成反比,所以蓝色的权重比较大,待预测样本被预测为蓝色方块。
(2)回归预测规则:一般采用平均值法或者加权平均值法
- 平均值法
- 每个邻近样本的权重是一样的,也就是说最终预测的结果为所有邻近样本的目标属性值的均值;
- 加权平均值法
- 每个邻近样本的权重是不一样的,一般情况下采用权重和距离成反比的方式来计算,也就是说在计算均值的时候进行加权操作;
3.基于KNN的实例:邮件分类
读取数据
实验数据集共11572封邮件,包括9648封非垃圾邮件,1924封垃圾邮件。首先对每封邮件进行文本预处理,便于数据分析。
邮件文本预处理
去除非字母符号
使用正则表达式将非文字类的符号,如标点符号、数字或特殊字符等去除。
data=re.sub(r'[^A-Za-z]', ' ', data)
将字母转换为小写
将字母转换为小写,同时由于英文单词之间以空格作为自然分界符,因此以空格为间隔直接进行分词。
str_list = data.lower().split()
去除停用词
将介词等对垃圾邮件判断无关的词移除,并在停词表中增加人名等使词典改进。
stop_words = set(stopwords.words('english'))
filtered_sentence = [w for w in str_list if not w in stop_words]
根据特征分词
对清理后的邮件文本单词列表进行词频统计。
np.unique(list(map(lambda t: len(str(t).strip()), df['date'])))#根据长度去特征
print(np.unique(list(map(lambda t: len(str(t).strip()), df['date']))))
np.unique(list(filter(lambda t: len(str(t).strip())==30, df['date'])))
print(np.unique(list(filter(lambda t: len(str(t).strip())==31, df['date']))))
def extract_email_date(str1):
if not isinstance(str1, str):
str1 = str(str1)
str_len = len(str1)
week = ""
hour = ""
time_quantum = ""
if str_len < 10:
week = "unknown"
hour = "unknown"
time_quantum = "unknown"
pass
elif str_len == 16:
rex = r"(\d{2}):\d{2}"#只取冒号前面的
it = re.findall(rex, str1)
if len(it) == 1:
hour = it[0]
else:
hour = "unknown"
week = "Fri"
time_quantum = "0"
pass
elif str_len == 19: #['Sep 23 2005 1:04 AM']
week = "Sep"
hour = "01"
time_quantum = "3"
pass
elif str_len == 21: #['August 24 2005 5:00pm'
week ="Wed"
hour = "17"
time_quantum = "1"
pass
else: #'Fri 2 Sep 2005 08:17:50' Wed 31 Aug 2005 15:06:36
rex = r"([A-Za-z]+\d?[A-Za-z]*) .*?(\d{2}):\d{2}:\d{2}.*"# 加问号保险些# 'Fri 23 Sep 2005 09:39:39 +0800 X-Priority: 3 X-Mailer: FoxMail'
it = re.findall(rex, str1)
if len(it) == 1 and len(it[0]) ==2:
week = it[0][0][-3:]
hour = it[0][1]
int_hour = int(hour)
if int_hour <8:
time_quantum = "3"
elif int_hour <13:
time_quantum = "0"
elif int_hour <19:
time_quantum = "1"
else:
time_quantum = "2"
pass
else:
week = "unknown"
hour = "unknown"
time_quantum = 'unknown'
week = week.lower()
hour = hour.lower()
time_quantum = time_quantum.lower()
return(week, hour, time_quantum)
#数据转换
date_time_extract_result = list(map(lambda st: extract_email_date(st), df['date']))
df['date_week'] = pd.Series(map(lambda t: t[0], date_time_extract_result))
df['date_hour'] = pd.Series(map(lambda t: t[1], date_time_extract_result))
df['date_time_quantum'] = pd.Series(map(lambda t: t[2], date_time_extract_result))
print(df.head(4))
print("======星期属性字段的描述==========")
print(df.date_week.value_counts().head(3))
print(df[['date_week', 'label']].groupby(['date_week', 'label'])['label'].count())
print("======小时属性字段的描述==========")
print(df.date_hour.value_counts().head(3))
print(df[['date_hour', 'label']].groupby(['date_hour', 'label'])['label'].count())
print("======时间段属性字段的描述==========")
print(df.date_hour.value_counts().head(3))
print(df[['date_time_quantum', 'label']].groupby(['date_time_quantum', 'label'])['label'].count())
df['has_date'] = df.apply(lambda c: 0 if c['date_week'] == 'unknown' else 1, axis=1)
print(df.head(4))
#===========================开始分词==============================================
print('='*30 + '现在开始分词,请耐心等待5分钟。。。' + '='*20)
df['content'] = df['content'].astype('str')
df['jieba_cut_content'] = list(map(lambda st: " ".join(jieba.cut(st)), df['content']))
df.head(4)
长度特征提取
#特征工程之四 长度提取
def precess_content_length(lg):
if lg <= 10:
return 0
elif lg <= 100:
return 1
elif lg <= 500:
return 2
elif lg <= 1000:
return 3
elif lg <= 1500:
return 4
elif lg <= 2000:
return 5
elif lg <= 2500:
return 6
elif lg <= 3000:
return 7
elif lg <= 4000:
return 8
elif lg <= 5000:
return 9
elif lg <= 10000:
return 10
elif lg <= 20000:
return 11
elif lg <= 30000:
return 12
elif lg <= 50000:
return 13
else:
return 14
df['content_length'] = pd.Series(map(lambda st:len(st), df['content']))
df['content_length_type'] = pd.Series(map(lambda st: precess_content_length(st), df['content_length']))
# print(df.head(10)) #如果不count就按照自然顺序排
df2 = df.groupby(['content_length_type', 'label'])['label'].agg(['count']).reset_index()#agg 计算并且添加count用于后续计算
df3 = df2[df2.label == 1][['content_length_type', 'count']].rename(columns = {'count' : 'c1'})
df4 = df2[df2.label == 0][['content_length_type', 'count']].rename(columns = {'count' : 'c2'})
df5 = pd.merge(df3, df4)#注意pandas中merge与concat的区别
df5['c1_rage'] = df5.apply(lambda r: r['c1'] / (r['c1'] + r['c2']), axis = 1)
df5['c2_rage'] = df5.apply(lambda r: r['c2'] / (r['c1'] + r['c2']), axis = 1)
print(df5)
模型训练预测
使用scikit-learn机器学习库训练分类器,输入样本数据和结构化的输出结果,运行k-近邻算法(KNN)判定输入数据属于哪一个分类。由于3000个单词属性的数值对于计算结果的影响是相等的,数据已经在同一尺度,因此可以不用进行数值归一化,而是直接将向量放入KNN分类器中,在试验中准确率无变化也说明其对结果无明显影响。
首先,提供数据集的90%作为训练样本来训练分类器,而使用其余的10%数据去测试分类器,检测分类器的正确率。
之后,在运行算法前,调整并指定超参数:使用交叉验证法,得到最优K值,此时K=4,即计算距离后,选取距离最小的前4个点,选择这4个最相似数据中出现次数最多的分类,作为新数据的分类。在多次试验后,选取weights = ‘uniform’,即不考虑距离权重这个超参数,所有的邻近点的权重都是相等的;选取p=1,使用曼哈顿距离计算两点距离,而非欧式距离。
最后算法结论得出,错误率在可接受范围内,则可以运行k-近邻算法进行分类。
部分代码
knn的模型预测代码
#-*- coding:utf-8 -*-
import pandas as pd
import numpy as np
import jieba
import time
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, recall_score
from sklearn.ensemble.tests.test_forest import check_min_samples_leaf
df = pd.read_csv('./data/result_process02', sep =',')
# print(df.head(5))
df.dropna(axis = 0, how ='any', inplace = True)
# print(df.head(5))
# print(df.info())
x_train, x_test, y_train, y_test = train_test_split(df[['has_date','jieba_cut_content',\
'content_length_sema']],df['label'],\
test_size = 0.2, random_state = 0)
print("训练集大小%d" % x_train.shape[0])
print("测试集大小%d" % x_test.shape[0])
# print(x_train.head(1000))
# print(x_test.head(10))
# #================================================================================================
print('开始训练集的特征工程:')
start_time_train = time.time()
transformer = TfidfVectorizer(norm = 'l2', use_idf = True)
svd = TruncatedSVD(n_components=20)
jieba_cut_content = list(x_train['jieba_cut_content'].astype('str'))
transformer_model = transformer.fit(jieba_cut_content)#Tf-idf
df1 = transformer_model.transform(jieba_cut_content)
svd_model = svd.fit(df1)
df2 = svd_model.transform(df1)
data = pd.DataFrame(df2)
end_time_train = time.time()
print('Running train_svd time: %.2s second(s)'%(end_time_train-start_time_train))
# print(data.head(10))
# print(data.info())
#
data['has_date'] = list(x_train['has_date'])
data['content_length_sema'] = list(x_train['content_length_sema'])
# print(data.head(10))
# print(data.info())
#
print('开始KNN模型训练:')
knn = KNeighborsClassifier(n_neighbors=5)
model = knn.fit(data, y_train)
#测试集处理
jieba_cut_content_test = list(x_test['jieba_cut_content'].astype('str'))
data_test = pd.DataFrame(svd_model.transform(transformer_model.transform(jieba_cut_content_test)))
data_test['has_date'] = list(x_test['has_date'])
data_test['content_length_sema'] = list(x_test['content_length_sema'])
# print(data_test.head(10))
# print(data_test.info())
start_time_KNN = time.time()
y_predict = model.predict(data_test)
#
precision = precision_score(y_test, y_predict)
recall = recall_score(y_test, y_predict)
f1mean = f1_score(y_test, y_predict)
print('精确率为:%0.5f' % precision)
print('召回率为:%0.5f' % recall)
end_time_KNN = time.time()
print('Running KNN_model time: %.2s second(s)'%(end_time_KNN-start_time_KNN))
结果
总结
1.垃圾邮件过滤系统中一般采用算法过滤+其它过滤统计结合的方式来进行垃圾邮件过滤。
2.在垃圾邮件过滤中主要是需要进行分词操作。
3.在垃圾邮件过滤中一般注意召回率,也就是说一般情况下,需要尽可能的提高垃圾邮件过滤的成功率。