数据挖掘(九)
文章目录
已知目标类别的学习任务,叫做监督学习。不知道类别的情况下进行数据挖掘,这叫做无监督学习,偏重于探索、发现隐藏在数据里的信息,而不是用模型来分类。
获取新闻文章
本文讨论构建一个按照主题为最新的新闻报道分组的系统。
使用Web API获取数据
- 使用Web API采集数据,我们需要注意授权方法、请求频率限制和API端点。
- 授权方法时数据提供方用来管理数据采集方的。数据提供方以此来了解谁正在采集数据,确保采集方抓取数据的频率没有超出上限,同时对采集方都采集了哪些数据可以做到监视。对于多数网站来说,普通个人账号就能用来采集数据,但也有部分网站要求采集方使用正式的开发者账号。
- 采集频率限制规定了采集方在约定时间内的最大请求次数,特别是对于免费提供的服务。在使用数据获取接口时,一定要了解不同的网站可能有着不同的规定。即使同一个网站,不同的API调用也有不同的采集频率限制。
- API端点是指用来抽取信息的实际网址。不同网站提供不同的接口,大部分Web API提供Restful接口。
从任意网站抽取文本
我们从Hao123收集的网址所指向的网站分属不同的网站组织。当我们尝试用程序获取里面的实际内容时,可能遇到各种困难,比如有很多逻辑是在后台运行的,调用JS库,应用样式表,用AJAX加载广告,在侧边栏增加很多内容等,这些功能增加了网站的复杂程度,增加了自动采集信息的难度。
寻找任意网站网页中的主要内容
- 首先访问每个链接,下载各个网页,把它们保存到数据文件夹中事先建好的用于存放原始网页的文件夹raw,然后我们从这些原始网页中获取有用的信息。我们使用MD5散列算法为每篇报道创建一个唯一的文件名。散列函数将输入转换为一个看似随机产生的字符串。对于相同的输入,散列函数返回相同的结果,并且散列函数是单向函数,根据散列值无法得到原来的值。
import os
import hashlib
import requests
stories = [['“旅行者”来做客,“美食”多样!唯美生态画卷颜值不断被“刷新”', 'http://baijiahao.baidu.com/s?id=1815151290164989110', '12'],
['网友称镇卫生院有人“吃空饷”,相关人员应被追责;珠海金湾区卫健局:重复领取', 'http://baijiahao.baidu.com/s?id=1815068325137550619', '14'],
['泼天流量说来就来,这次大学生“夜袭开封”', 'http://baijiahao.baidu.com/s?id=1815025933758571293', '18']]
data_folder = os.path.join(os.getcwd(), 'data_mining', 'news', 'raws')
# 对于网页下载失败的网站,直接跳过,维护计数器统计下载失败的次数,如果是系统本身的问题阻止下载,我们需要解决问题。如果失败次数过多,我们就需要找出到底是什么问题并解决
number_errors = 0
# 遍历每篇新闻报道
for title, url, score in stories:
# 对标题进行散列操作
output_filename = hashlib.md5(url.encode()).hexdigest()
fullpath = os.path.join(data_folder, output_filename + '.txt')
try:
# 下载网页保存到输出文件夹
response = requests.get(url)
data = response.text
with open(fullpath, 'w') as outf:
outf.write(data)
except Exception as e:
number_errors += 1
# 如果异常过多,调用raise处理异常中断机制
print(e)
- 获得原始网页后,我们需要找出每个网页中的新闻报道内容,有些在线资源使用数据挖掘方法来解决这个问题。从网页中抽取文本代码可以使用lxml包解析HTML文件,lxml的HTML解析器容错能力强,可以处理不规范的HTML代码。文本抽取首先遍历HTML文件的每个节点,抽取其中的文本内容;其次跳过JS、样式和注释节点,这些系欸但不太可能包含对我有有价值的信息。最后确保文本内容长度至少为100字符。文本抽取函数在任何子节点上调用自己来抽取子节点中的文本内容,最后返回拼接在一起的所有子节点的文本。如果一个节点没有任何子节点,文本抽取函数返回该节点的文本内容,如果该节点不包含任何内容,就返回空字符串。
# 获得raws文件夹中的所有文件名
filenames = [os.path.join(data_folder, filename) for filename in os.listdir(data_folder)]
from lxml import etree, html
# 存放不可能包含新闻报道内容的节点
skip_node_types = ['script', 'head', 'style', etree.Comment]
# 把html文件解析成lxml etree对象
def get_text_from_file(filename):
with open(filename) as inf:
html_tree = html.parse(inf)
# 调用getroot()函数获取树的根节点,这样以这个节点作为入参,能处理包括根节点在内的所有节点
return get_text_from_node(html_tree.getroot())
# 对每个子节点递归调用文本抽取函数,把返回后的文本内容拼接在一起
def get_text_from_node(node):
if len(node) == 0:
if node.text and len(node.text) > 100:
return node.text
else:
return ''
results = (get_text_from_node(child) for child in node if child.tag not in skip_node_types)
# 防止返回空行
return '\n'.join(r for r in results if len(r) > 1)
# 从文件中抽取文本
for filename in os.listdir(data_folder):
text = get_text_from_file(os.path.join(data_folder, filename))
with open(os.path.join(data_folder, filename), 'w') as outf:
outf.write(text)
示例代码1
import os
import hashlib
import requests
from lxml import etree, html
stories = [['“旅行者”来做客,“美食”多样!唯美生态画卷颜值不断被“刷新”', 'http://baijiahao.baidu.com/s?id=1815151290164989110', '12'],
['网友称镇卫生院有人“吃空饷”,相关人员应被追责;珠海金湾区卫健局:重复领取', 'http://baijiahao.baidu.com/s?id=1815068325137550619', '14'],
['泼天流量说来就来,这次大学生“夜袭开封”', 'http://baijiahao.baidu.com/s?id=1815025933758571293', '18']]
data_folder = os.path.join(os.getcwd(), 'data_mining', 'news', 'raws')
number_errors = 0
for title, url, score in stories:
print(title, url, score)
output_filename = hashlib.md5(url.encode()).hexdigest()
fullpath = os.path.join(data_folder, output_filename + '.txt')
try:
response = requests.get(url)
response.encoding = 'utf-8'
data = response.text
with open(fullpath, 'w') as outf:
outf.write(data)
except Exception as e:
number_errors += 1
print(e)
documents = [open(os.path.join(data_folder, filename), encoding='utf-8').read() for filename in os.listdir(data_folder)]
skip_node_types = ['script', 'head', 'style', etree.Comment]
def get_text_from_file(filename):
with open(filename, encoding='utf-8') as inf:
html_tree = html.parse(inf)
return get_text_from_node(html_tree.getroot())
def get_text_from_node(node):
if len(node) == 0:
if node.text and len(node.text) > 100:
return node.text
else:
return ''
results = (get_text_from_node(child) for child in node if child.tag not in skip_node_types)
return '\n'.join(r for r in results if len(r) > 1)
for filename in os.listdir(data_folder):
text = get_text_from_file(os.path.join(data_folder, filename))
with open(os.path.join(data_folder, filename), 'w', encoding='utf-8') as outf:
outf.write(text)
新闻语料聚类
我们可以通过聚类发现新闻语料中潜藏的趋势。我们使用经典的机器学习算法k均值算法k-means。聚类属于无监督学习。假设我们使用500篇新闻报道组成的数据集,人工查看这些文章的主题费时费力,即使使用概括统计方法也不容易。聚类分析可以按照主题把它们分成不同的簇,然后可以按簇研究它们的主题。
k-means算法
k-means聚类算法迭代寻找最能够代表数据的聚类质心点。算法开始时使用从训练数据中随机选取的几个数据点作为质心点。k-means中的k表示寻找多少个质心点,同时也是算法将会找到的簇的数量。
- k-means算法分为两步,一是为每个数据点分配簇标签,二是更新各簇的质心点。一个数据点对应数据集中一条数据,把数据集看成样本,一条数据可以看作是一个个体。我们在分簇时为数据集中的个体设置一个标签,把它和最近的质心点联系起来。对于距离质心点1最近的个体,我们为它们分配标签1,以此类推。标签相同的个体属于同一个簇。更新环节计算各簇内所有数据点的均值,更新质心点。k-means算法会重复上述步骤,每次更新质心点时,所有质心点将会小范围移动。这个过程会执行知道条件不再满足位置。
- k-means算法只有一个参数,在很多数据挖掘问题上效果很好,所以仍然被频繁使用。scikit-learn实现了k-means算法。创建数据分析流水线,第一步是特征抽取,第二步是调用k-means算法。
from sklearn.cluster import KMeans
# 这个向量化工具根据词语出现在多少篇文档中对词语计数进行加权,出现在较多文档中的词语权重较低。用文档集数量除以词语出现在的文档的数量,然后取对数
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.pipeline import Pipeline
from collections import Counter
# 参数max_df=0.4表示忽略出现在40%及以上的文档中的词语,可以用来剔除本身不含有主题相关含义的词语
documents = [open(os.path.join(data_folder, filename), encoding='utf-8').read() for filename in os.listdir(data_folder)]
n_clusters = 2
pipeline = Pipeline([('feature_extraction', TfidfVectorizer(max_df=0.4)), ('clusterer', KMeans(n_clusters))])
pipeline.fit(documents)
# labels变量包含每个数据点的簇标签
labels = pipeline.predict(documents)
# 使用Counter类来查看每个簇有多少个数据点
c = Counter(labels)
for cluster_number in range(n_clusters):
print('Cluster {} contains {} samples'.format(cluster_number, c[cluster_number]))
评估结果
- 聚类分析主要是探索性分析,因此很难有效地评估聚类算法结果的好坏。评估算法结果最直接的方式是根据它要学习的标准对其进行评价。对于k均值算法,寻找新质心点的标准是最小化每个数据点到最近质心点的距离,这叫做算法的惯性权重。
# 任何经过训练的KMeans实例都有惯性权重,可以用来确定分多少簇合适
pipeline.named_steps['clusterer'].inertia_
# n_cluster_values依次取2到20之间的值,每取一个值,k-means算法运行10此,每次运行算法都记录惯性权重,每次仅训练X矩阵一次
inertia_scores = []
n_cluster_values = list(range(2, 20))
for n_clusters in n_cluster_values:
cur_inertia_scores = []
X = TfidfVectorizer(max_df=0.4).fit_transform(documents)
for i in range(40):
km = KMeans(n_clusters).fit(X)
cur_inertia_scores.append(km.inertia_)
inertia_scores.append(cur_inertia_scores)
# 变量inertia_scores存储了n_clusters取2到20每个值时所对应的惯性权重,把惯性权重和簇的数量做成图。随着簇数量增加,质心点和其他数据点位置的调整逐渐减少,惯性权重应该逐渐降低
from matplotlib import pyplot as plt
inertia_means = np.mean(inertia_scores, axis=1)
inertia_stderr = np.std(inertia_scores, axis=1)
fig = plt.figure(figsize=(40,20))
plt.errorbar(n_cluster_values, inertia_means, inertia_stderr, color='green')
plt.show()
从簇中抽取主题信息
- 我们尝试找到每个簇的主题,首先从特征提取这一步抽取词表。其次k均值算法可以用来简化特征。特征简化的方法有很多种,比如主要成分分析、潜在语义索引等,这些方法还能用来创建新特征。
# 从流水线的特征提取这一步抽取词条
terms = pipeline.named_steps['feature_extraction'].get_feature_names()
# 计算每簇所包含的个体数量
c = Counter(labels)
# 遍历所有的簇,输出每簇所包含的个体数量
for cluster_number in range(n_clusters):
print("Cluster {} contains {} samples".format(cluster_number, c[cluster_number]))
print(" Most important terms")
# 从质心点找出特征值最大的5个特征
centroid = pipeline.named_steps['clusterer'].cluster_centers_[cluster_number]
most_important = centroid.argsort()
for i in range(5):
term_index = most_important[-(i+1)]
print(" {0}) {1} (score: {2:.4f})".format(i+1, terms[term_index], centroid[term_index]))
聚类融合
聚类算法可以进行融合,融合后得到的算法能够平滑算法多次运行所得到的不同结果。多次运行k均值算法得到结果因为最初选择的质心点不同而不同。聚类融合方法可以降低参数选择对最终结果的影响。
证据累积
- 基本的融合方法是对数据进行多次聚类,每次都记录各个数据点的簇标签,然后计算每两个数据点被分到同一个簇的次数。这就是证据累积算法的精髓。第一步是使用k-means等低水平的聚类算法对数据集进行多次聚类,记录每次跌倒两个数据点出现在同一簇的频率,将结果保存到共协矩阵中。第二步是是用另一种聚类算法,分级聚类对第一步得到的共协矩阵进行聚类分析。
from scipy.sparse import csr_matrix
# 遍历所有标签,记录具有相同标签的两个数据点的位置,创建共协矩阵
def create_coassociation_matrix(labels):
rows = []
cols = []
unique_labels = set(labels)
for label in unique_labels:
indices = np.where(labels == label)[0]
for index1 in indices:
for index2 in indices:
rows.append(index1)
cols.append(index2)
data = np.ones((len(rows),))
return csr_matrix((data, (rows, cols)), dtype='float')
C = create_coassociation_matrix(labels)
- 对共协矩阵进行分级聚类。我们需要找到该矩阵的最小生成树MST,删除权重低于阈值的边。
from scipy.sparse.csgraph import minimum_spanning_tree
# 矩阵C中值越高表示一组数据点被分到同一簇的次数越多,这个值表示相似度
mst = minimum_spanning_tree(C)
pipeline.fit(documents)
label2s = pipeline.predict(documents)
C2 = create_coassociation_matrix(labels2)
C_sum = (C + C2)/2
mst = minimum_spanning_tree(-C_sum)
# 删除两个矩阵中都没有出现的边
mst.data[mst.data > -1] = 0
mst.eliminate_zeros()
# 找到所有的连通分支,寻找移除低权重的边以后仍然连接在一起的节点
from scipy.sparse.csgraph import connected_components
number_of_clusters, labels = connected_components(mst)
工作原理
- k-means算法不考虑特征的权重,假定所有的特征取值范围相同,寻找的是圆形簇。证据累积算法的工作原理是重新把特征映射到新空间,每次运行k-means算法相当于使用转换器对特征进行一次转换。证据累积算法只关心数据点之间的距离而不是它们在原来特征空间的位置。对于没有规范化过的特征,仍然存在问题。我们使用tf-idf规范特征值,从而使特征具有相同的值域。
from sklearn.base import BaseEstimator, ClusterMixin
# 创建证据累积算法类
class EAC(BaseEstimator, ClusterMixin):
# k-means算法运行次数,用来删除边的阈值和每次运行算法要找到的簇的数量,指定取值范围
def __init__(self, n_clusterings=10, cut_threshold=0.5, n_clusters_range=(3, 10)):
self.n_clusterings = n_clusterings
self.cut_threshold = cut_threshold
self.n_clusters_range = n_clusters_range
def fit(self, X, y=None):
# 使用低水平聚类把每次迭代得到的共协矩阵加起来,为了节省内存,我们使用生成器,仅在需要时创建共协矩阵
C = sum((create_coassociation_matrix(self._single_clustering(X))
for i in range(self.n_clusterings)))
mst = minimum_spanning_tree(-C)
mst.data[mst.data > -self.cut_threshold] = 0
mst.eliminate_zeros()
self.n_components, self.labels_ = connected_components(mst)
return self
def _single_clustering(self, X):
n_clusters = np.random.randint(*self.n_clusters_range)
km = KMeans(n_clusters=n_clusters)
return km.fit_predict(X)
def fit_predict(self, X):
self.fit(X)
return self.labels_
线上学习
线上学习是指用新数据增量地改进模型。支持线上学习的算法可以先用一条或少量数据进行训练,随着更多新数据的添加,更新模型。标准的k-means算法不支持线上学习。线上学习算法在只有几条新数据的情况下就能做到部分更新已有模型。神经网络算法使支持线上学习的标准例子。随着一条心数据插入到神经网络后,网络中的权重根据学习速率进行更新,学习速率通常使一个很小的值。神经网络还可以按照批模式来训练,每次只用一组数据进行训练。我们可以用一个数据点或少量数据点来轻微更新k-means中的质心点。
- scikit-learn提供了MiniBatchKBeans算法用来实现线上学习功能。
from sklearn.cluster import MiniBatchKMeans
# 从数据集中抽取特征,创建矩阵X
vec = TfidfVectorizer(max_df=0.4)
X = vec.fit_transform(documents)
mbkm = MiniBatchKBeans(random_state=14, n_clusters=3)
# 随机从X矩阵中选择数据,模拟来自外部的新数据
batch_size = 10
for iteration in range(int(X.shape[0] / batch_size)):
start = batch_size * iteration
end = batch_size * (iteration + 1)
mbkm.partial_fit(X[start:end])
# 获得原始数据集的聚类结果
labels_mbkm = mbkm.predict(X)
mbkm.inertia_