机器学习实战(url分类)
实际应用中,我们经常需要对集合进行分类,一方面可以进行统计,另一方面也可以用作去重。常见的例如url分类(本质上是字符串分类,不过又因为url的特殊性使其需要特别处理),地址分类(经纬度分类),文章分类(文章分类我们之后会介绍)。分类的其中一种思路是先把集合量化为几项特征值,使其可以计算出个体间的距离。然后通过分类算法(这里使用的是k-means算法)进行分类。
这里举的例子是url分类,我们从爬虫获取到url,需要把它们分成不同的类别。我们先分析一个url:
https://www.example.com/abc/123?id=1
我们可以把url抽象成几个特征:
http还是https,域名,目录,目录深度,参数,参数数量,域名在Alexa上的排名。域名在搜索引擎的收录数
这一步非常重要,有些开发者往往只在乎算法的效率和实现,忽略了从需求角度去对数据进行分析和提取。就像理查德费曼所说的,“无论计算机多少神奇,你给它的是垃圾,它出来的也是垃圾”。
分析以上的url我们可以得到:
- 访问安全性:https
- 域名为:www.example.com
- 目录:abc/123
- 目录深度:三级
- 参数:id=1
- 参数数量:1个
- Alexa排名:10000(非真实)
- 搜索引擎收录数:1000(非真实)
我们根据需求给这8个特征赋予不同的权重
Similarity = 域名 * 0.4 + 目录 * 0.2 + 目录深度 * 0.2 + 参数 * 0.1 + 参数数量 * 0.1
在这里我们只使用5个特征,特征的数量对具体实现并没有影响。不过这里需要注意一个问题,在计算距离的时候我们可以使用欧式距离(Euclidean Distance),编辑距离(Edit Distance),余弦相似性(Cosine Similarity)。欧式距离公式如下:
在这里我们可以看到,不同的特征由于它的绝对值范围不同(例如域名一般会在20个字符串内,而Alexa排名却可以在一到几百万的区间),对最终距离影响的范围也不同。例如在所有url中Alexa排名最前的拍在第100位,最后为2000位。那么这两个url的距离因为排名这个特征被过度放大了。所以在计算距离之前,我们常常需要归一化数值,将特征值的取值范围局限在0到1或者-1到1之间,公式如下:
newValue = (oldValue - min) / (max - min)
假如集合中一个url的排名为500,那么在计算过之后就变成
newValue = (500 - 100) / (2000 - 100) = 0.21
之后可以计算url之间每个特征的编辑距离。
https://www.example.com/abc/123?id=1
https://www.example.com/abc/cdf/258?id=19
这里的编辑距离分别为,0,6,1,1,0,(这是归一化数值之前的编辑距离,实际应用需要先获得每个特征值的距离最大最小值,之后再把编辑距离转化为0到1之间)计算编辑距离的代码如下:
import numpy as np
def levenshtein(source, target):
''' 使用矩阵计算比其他方法快40% '''
if len(source) = len(target).
if len(target) == 0:
return len(source)
# We call tuple() to force strings to be used as sequences
# ('c', 'a', 't', 's') - numpy uses them as values by default.
source = np.array(tuple(source))
target = np.array(tuple(target))
# We use a dynamic programming algorithm, but with the
# added optimization that we only need the last two rows
# of the matrix.
previous_row = np.arange(target.size + 1)
for s in source:
# Insertion (target grows longer than source):
current_row = previous_row + 1
# Substitution or matching:
# Target and source items are aligned, and either
# are different (cost of 1), or are the same (cost of 0).
current_row[1:] = np.minimum(
current_row[1:],
np.add(previous_row[:-1], target != s))
# Deletion (target grows shorter than source):
current_row[1:] = np.minimum(
current_row[1:],
current_row[0:-1] + 1)
previous_row = current_row
return previous_row[-1]
其他实现方法可以参考编辑距离
知道怎么计算距离,就可以使用分类算法,简单的可以使用k-means。这里为了讲解,并不是用numpy库
import random
import functools
from ml_logging import setup_logging
setup_logging(__name__)
def read_from_file(file_name, dimension_num, split_char):
'''从文件读取数据
file_name: 数据源文件名
dimension_num: 选择的数据维度数量
split_char: 数据分隔符
'''
def list_to_float(
line, dimension_num=dimension_num, split_char=split_char):
l = [float(x) for x in line.replace("\n", "").split(
split_char)[:dimension_num]]
return l
try:
with open(file_name, 'r') as data:
# 若每行数据源为a1 a2 a3
# 返回格式为float数值
# [[a1, a2, a3], [b1, b2, b3], [c1, c2, c3]]
dataset = [list_to_float(x) for x in data]
except IOError:
pass
return dataset
def cluster_random(dataset, chuster_num):
'''随机选择质点
dataset: 数据源
chuster_num: 质点数量
'''
return random.sample(dataset, chuster_num)
def cal_distance(a, b):
'''计算两个数据源之间的欧几里得距离
dataset: 数据源
chuster_num: 质点数量
'''
return functools.reduce(
lambda x, y: x+y, [(x - y) ** 2 for x, y in zip(a, b)]) ** 0.5
def cal_multi(cluster_list, dataset):
'''把每一个点分到最近的质心,返回分组后的情况
cluster_list: 质心列表
dataset: 数据源
'''
di = {}
for i in dataset:
ans = list(map(lambda x: cal_distance(i, x), cluster_list))
cluster_index = ans.index(min(ans))
di.setdefault(cluster_index, []).append(i)
# di返回的格式应该如下:
# di = {
# 1: [[a1, a2], [c1, c2]],
# 2: [[b1, b2], [d1, d2]]
# }
return di
def new_cluster(cluster_dict):
'''获取新的聚簇点
cluster_dict: 包含分组信息的字典
'''
def average(nums):
return sum(nums)/len(nums)
return list(
map(lambda x: (average(n) for n in zip(*x.values())), cluster_dict))
def main(file_name, dimension_num, split_char, k):
# 读取数据
dataset = read_from_file(file_name, dimension_num, split_char)
# 选择初始的k个质点
cluster_list = cluster_random(dataset, k)
# 这是第一次分组
cluster_before = False
# 默认分组为空
cluster_dict = {}
while 1:
if cluster_before:
# 根据现有分组情况计算新的质心
new_cluster(cluster_dict)
else:
cur_dict = cal_multi(cluster_list, dataset)
cluster_before = True
if cur_dict == cluster_dict:
break
else:
cluster_dict = cur_dict
使用numpy实现可以参考python的k-means实现
这里使用k-means算法,太受随机起始点的影响,可以结合二分k-means和k-means++进行分类,使分类更加稳定。