分类目录归档:Python 数据分析

Python 预测广东省2019年12月猪肉价格

前段时间我们用回归的方法预测了天猫的2019年销售额:《Python 多项式预测2019年天猫销售额》以及《人口出生率预测》 。今天我们将使用Facebook的“先知”模型来预测12月的猪价。

Python实用宝典其实已经用过“先知”模型,大家可以阅读这篇文章:《Python使用”先知”预测公众号未来的粉丝量》,当时预测公众号的粉丝在一个月后最多增长到547人,实际增长到了542人,预测地相当准确,而且是在我的宣传模式做了一些改变的情况下的效果,可见这个模型能够很好地处理一些异常变化。

今天我们将使用先知模型预测2019年12月广东省的猪肉价格,源代码和数据你都能通过关注文章最下方的公众号,后台回复:猪价预测 获得。本实验仅供参考。

1.准备数据

在猪价系统网站上利用开发者工具获得过去一年广东省的猪肉价格保存为json格式:
https://zhujia.zhuwang.cc/areapriceinfo-440000.shtml

部分数据如下:

实际上我认为,就猪价这样的对象,拿一年的数据是远远不够的,但是实在找不到前几年的数据。作为一次实验,我暂时以过去一年的数据作为训练集,如果你想要更精准地预测价格,建议至少找3年的数据。

2.数据预测

以下教程默认你已经安装好了Python并可以在CMD或Terminal中使用pip,如果没有请看这篇文章:安装python

2.1 安装 “先知” prophet

Prophet这个包真是一言难尽,如果你按照官方的教程来进行安装, 你会发现啥也安装不上(我吐了)。这里给大家介绍我的安装方法,避免你们走弯路:

第一步,我们需要安装fbprophet的依赖PyStan:

pip install pystan

第二步,使用conda命令安装(需要安装anaconda, 搜anaconda官网安装即可):

conda install -c conda-forge fbprophet

2.2 编写预测代码

首先,利用开发者工具弄下来的数据缺少日期,我们需要获得过去365天的日期,并与原数据对应上:

# 获得2019-11-20过去365天的数据
days = []
today = datetime.date.today()
for i in range(0,366,1):
    daybeforetoday = today + datetime.timedelta(days=-i)
    days.append(daybeforetoday.strftime('%Y-%m-%d'))
days = list(reversed(days))
print(days) 

然后我们将猪价提取出来,并将日期和猪价转换为pandas的DataFrame格式:

f = open('./data_20191120.json', 'r', encoding='utf-8')
json_data = json.load(f)
f.close()
# 提取猪价
list_number = json_data['pigprice']
print(len(list_number), len(days))
# prophet模型预测前需要将日期列设为ds,预测的值设为y
df = pd.DataFrame({'y':list_number, 'ds':days})
print(len(df), len(days))

最后,调用先知模型进行预测,这里我们只预测30天,所以periods设为了30:

from fbprophet import Prophet

# 调用"先知"生成对象
m = Prophet()

# 使用"先知对象"进行预测
m.fit(df)

# 获得未来30天的数据
future = m.make_future_dataframe(periods=30)

forecast = m.predict(future)
print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())  

结果如下:

根据过去一年(可能不够)的猪肉价格数据,预测到在下个月的今天,猪肉将会上涨到49元/公斤左右的价格。由于数据量实在是太少了,检测不出每一年的猪肉波动性,因此这个预测得到的数据我估计误差比较大,但是大家只需要知道先知模型的使用方法,本文就值了。

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 人口出生率数据预测及可视化

上次我们用多项式回归预测了天猫的2019年销售额:《Python 多项式预测2019年天猫销售额》。今天的文章同样教大家怎么进行预测,不过对象换成2019年人口出生率。

今天我们将使用多项式回归模型预测2019年的中国人口出生率。

1.准备

在中华人民共和国国家统计局官方网站上下载过去20年的人口出生率数据:http://data.stats.gov.cn/easyquery.htm?cn=C01

数据如下,我们仅取人口出生率作为我们的数据集。

2.数据预测

以下教程默认你已经安装好了Python并可以在CMD或Terminal中使用pip,如果没有请看这篇文章:安装python,同上一节预测2019年天猫销售额一样,没有太多的改变,但是我们需要找到最合适的曲线,而不是一上来就用三元回归曲线:

2.1 数据预处理

# 数据集
datasets_X = [1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018]
datasets_Y = [14.64, 14.03, 13.38, 12.86, 12.41, 12.29, 12.4, 12.09, 12.1, 12.14, 11.95, 11.9, 11.93, 12.1, 12.08, 12.37, 12.07, 12.95, 12.43, 10.94]
test_X = [2019]
 
# 数据预处理
dataset_length = len(datasets_X)
test_length = len(test_X)
# 将数据转化为numpy数组
datasets_X = np.array(datasets_X).reshape([dataset_length, 1])
test_X = np.array(test_X).reshape([test_length, 1])
datasets_Y = np.array(datasets_Y)

2.2 数据可视化

将已有的数据显示出来,并用蓝点作为标记,将每一年的横坐标都显示出来。曲线我们从二元回归曲线开始慢慢找到最符合数据趋势的曲线。

# 构造多项式特征
poly_reg = PolynomialFeatures(degree=2)
X_poly = poly_reg.fit_transform(datasets_X)
 
# 使用线性回归模型学习X_poly和datasets_Y之间的映射关系
lin_reg = LinearRegression()
lin_reg.fit(X_poly, datasets_Y)

# 数据可视化

# 蓝色显示训练数据点
plt.scatter(datasets_X, datasets_Y, color='blue')

# X轴
my_x_ticks = np.arange(1998, 2020, 1)
plt.xticks(my_x_ticks)

# 绘制线
X = np.arange(1998, 2020).reshape([-1, 1])
plt.plot(X, lin_reg.predict(poly_reg.fit_transform(X)), color='black')

plt.xlabel('Years')
plt.ylabel('Born rate')
plt.show() 

运行代码结果如下:

二元回归曲线

显然二元回归曲线并不是非常适合这些数据,有许多点分布在离直线很远的距离。让我们试试三元回归曲线,修改poly_reg变量的degree,设为3:

poly_reg = PolynomialFeatures(degree=3)

结果如下:

三元回归曲线

感觉有辣么点意思了,再试试四元回归曲线:

四元回归曲线

发现和三元回归曲线相比没有太大的变化,那我们选三元回归曲线进行预测就可以了。

2.3 数据建模

# 数据建模
# 构造三次多项式特征
poly_reg = PolynomialFeatures(degree=3)
X_poly = poly_reg.fit_transform(datasets_X)
 
# 使用线性回归模型学习X_poly和datasets_Y之间的映射关系
lin_reg_3 = LinearRegression()
lin_reg_3.fit(X_poly, datasets_Y)
 
data = poly_reg.fit_transform(test_X)
pred = lin_reg_3.predict(data)
print(pred)

预测的2019人口出生率的值为:[ 11.25692317],使用黄点表示该预测的值,得到的曲线图如下:

似乎是条挺合理的曲线,让我们拭目以待今年的人口出生率会不会是11.25,到时候记得翻出这篇文章来看看。

如果你喜欢今天的Python 教程,请持续关注Python实用宝典,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们会耐心解答的!

​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 多项式预测2019年天猫销售额

双十一刚过,天猫的销售额创新高占领了各大新闻媒体头条。但是,知乎上的一个问题对本次双十一的销售额提出了一个非常有意思的问题:《如何看待双十一销售额完美分布在三次回归曲线上且拟合高达 99.94%?是巧合还是造假?》

本文的重点放在如何用Python实现三次回归曲线的预测功能。

1.数据源

采用cnbeta新闻报道中的数据:《[直击]2019年天猫双11落幕:交易额2684亿元》

有效数字保留三位,其中2009年为0.52亿、2010年为9.36亿、2011年为52.0亿。

2.代码构建

我们将使用scikitlearn的多项式回归实现预测。训练集是2009至2014年的数据,后续测试中将陆续增加训练集至2018年。

2.1 数据预处理

构造三次多项式特征需要以列的形式,因此代码中会将numpy数组reshape成列形式。

# 数据集
datasets_X = [2009, 2010, 2011, 2012, 2013, 2014]
datasets_Y = [0.52, 9.36, 52.0, 191, 350, 571]
test_X = [2015, 2016, 2017, 2018, 2019]
real_Y =  [912, 1207, 1682, 2132, 2684]

# 数据预处理
dataset_length = len(datasets_X)
test_length = len(test_X)
# 将数据转化为numpy数组,并变为列的形式
datasets_X = np.array(datasets_X).reshape([dataset_length, 1])
test_X = np.array(test_X).reshape([test_length, 1])
datasets_Y = np.array(datasets_Y)
real_Y = np.array(real_Y)

2.2 数据建模训练

这里需要注意的是,必须先构建三次多项式特征后才可放入线性回归模型。

# 数据建模
# 构造三次多项式特征
poly_reg = PolynomialFeatures(degree=3)
X_poly = poly_reg.fit_transform(datasets_X)

# 使用线性回归模型学习X_poly和datasets_Y之间的映射关系
lin_reg_3 = LinearRegression()
lin_reg_3.fit(X_poly, datasets_Y) 

2.3 数据预测

训练完毕后,就可以将lin_reg_3模型用于预测:

data = poly_reg.fit_transform(test_X)
pred = lin_reg_3.predict(data)
print(pred) 

结果:

[ 830.64251041 1127.88919544 1457.47841358 1814.48141575 2193.96944141]

在这个训练集的基础上,预测2019年的销售额是2193亿,和真实的2684亿还是差了点。不过这样看不是很直观,让我们可视化一下数据,并逐渐增加训练集。

3.数据可视化

用 matplotlib 可以非常简单地实现这一步,设定X轴范围—绘制数据点—绘制线,最后加上横纵轴说明。

# 数据可视化
# X轴
X = np.arange(2009, 2020).reshape([-1, 1])
# 蓝色显示训练数据点
plt.scatter(datasets_X, datasets_Y, color='blue')
# 红色显示真实数据点
plt.scatter(test_X, real_Y, color='red')
# 黄色显示预测数据点
plt.scatter(test_X, pred, color='yellow')

plt.plot(X, lin_reg_3.predict(poly_reg.fit_transform(X)), color='black')
plt.xlabel('年份')
plt.ylabel('销售额(亿)')
plt.show() 

结果如下,蓝色点是训练值,红色点是真实值,而黄色的点是预测值:

训练集增加2015年的真实数据,结果如下:

有意思,真实值反而比预测值低了,说明2015年这一年的成交额非常优秀。

再增加2016年的真实数据到训练集里看一下。

现在,2017年、2018年的预测值非常接近,预测2019年的销售额是2507.3990亿元,离真实的2684亿差了100多亿。让我们继续增加2017年的真实数据到训练集中,预测2018年和2019年的销售额。

优秀,不过2019年预测的销售额为2750亿,略高于真实值2684亿,我们最后将2018年真实值加入到训练集中,看看2019年该模型的预测值是多少。

惊呆了,小红点和小黄点完美重合。按照这个模型,2020年的双十一销售额将会是3293亿元。

关注文章最下方Python实用宝典公众号二维码,回复 天猫销售额预测 或阅读原文下载查阅本模型完整源代码:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import r2_score

# 指定默认字体否则图片显示不了中文
from pylab import mpl
mpl.rcParams['axes.unicode_minus'] = False # 解决符号显示问题
mpl.rcParams['font.sans-serif'] = ['FangSong']

# 数据集
datasets_X = [2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018]
datasets_Y = [0.52, 9.36, 52.0, 191, 350, 571, 912, 1207, 1682, 2132]
test_X = [2019]
real_Y =  [2684]

# 数据预处理
dataset_length = len(datasets_X)
test_length = len(test_X)
# 将数据转化为numpy数组
datasets_X = np.array(datasets_X).reshape([dataset_length, 1])
test_X = np.array(test_X).reshape([test_length, 1])
datasets_Y = np.array(datasets_Y)
real_Y = np.array(real_Y)

# 数据建模
# 构造三次多项式特征
poly_reg = PolynomialFeatures(degree=3)
X_poly = poly_reg.fit_transform(datasets_X)

# 使用线性回归模型学习X_poly和datasets_Y之间的映射关系
lin_reg_3 = LinearRegression()
lin_reg_3.fit(X_poly, datasets_Y)

data = poly_reg.fit_transform(test_X)
pred = lin_reg_3.predict(data)
print(pred)

# 数据可视化
# X轴
X = np.arange(2009, 2020).reshape([-1, 1])
# 蓝色显示训练数据点
plt.scatter(datasets_X, datasets_Y, color='blue')
# 红色显示真实数据点
plt.scatter(test_X, real_Y, color='red')
# 黄色显示预测数据点
plt.scatter(test_X, pred, color='yellow')

plt.plot(X, lin_reg_3.predict(poly_reg.fit_transform(X)), color='black')
plt.xlabel('年份')
plt.ylabel('销售额(亿)')
plt.show() 

如果你喜欢今天的Python 教程,请持续关注Python实用宝典,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们会耐心解答的!

​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 爬取“微博树洞”详细教程

最近要做一个关于自动从微博等短文本数据中判断人是否有自杀倾向的项目,在这之前需要先收集许多具有自杀倾向的人发的微博或短文本数据作为训练集。

其实这样的数据是挺难找的,尤其是对于我这种需求量比较大的项目。不过好在最后发现了突破口:“微博树洞”。“微博树洞”是指宣告了自杀行为的过世的人的微博,其留言区成为成千上万的抑郁症或是绝望的人的归属,在其下方发布许多负能量甚至是寻死的宣言。

比如走饭的微博:

1.找到微博评论数据接口

微博评论的数据接口有两种,一种是手机版、一种是PC版。手机版能爬到的数据仅仅只有十五页,因此我们从PC版入手,先来看看PC版的接口怎么找,长啥样儿。

首先,在当前微博的页面右键—检查(F12)打开开发者工具,然后按照下图的步骤进行操作(选择NetWork—选择XHR—随便点击另一个评论页—查看右侧新增的请求):

然后我们看新增的请求,你会发现在Preview中能看到格式化后的数据,而且里面有个html,仔细观察这个html你会发现这个就是评论列表的数据。我们仅需要将这个html解析出来即可。

再看看get请求的URL:

https://weibo.com/aj/v6/comment/big?ajwvr=6&id=3424883176420210&page=2&__rnd=1573219876141

ajwvr是一个固定值为6、id是指想要爬取评论的微博id、page是指第几页评论、_rnd是请求时的毫秒级时间戳。

不过微博是要求登录才能看更多评论的,因此我们需要先访问微博,拿到cookie的值才能开始爬。

2.编写爬虫

关注文章最下方的Python实用宝典,回复微博评论爬虫即可获得本项目的完整源代码。

设定四个参数:

    params = {
        'ajwvr': 6,
        'id': '3424883176420210',
        'page': 1,
        '_rnd': int(round(time.time() * 1000))
    } 

设定cookie:

headers = { 
    'X-Requested-With': 'XMLHttpRequest', 
    'Cookie': '你的微博cookie',
} 

发送请求并解析数据

    URL = 'https://weibo.com/aj/v6/comment/big'

    for num in range(1,51,1):
        print(f'============== 正在爬取第 {num} 页 ====================')
        params['page'] = num
        params['_rnd'] = int(round(time.time() * 1000))
        resp = requests.get(URL, params=params, headers=headers)
        resp = json.loads(resp.text) 

解析这串HTML中我们所需要的数据,这里用到了XPATH,如果你还不了解XPATH,可以看这篇文章: 学爬虫利器XPath,看这一篇就够了

         if resp['code'] == '100000':
            html = resp['data']['html']
            html = etree.HTML(html)
            data = html.xpath('//div[@node-type="comment_list"]')
            for i in data:
                # 评论人昵称
                nick_name = i.xpath('.//div[@class="WB_text"]/a[1]/text()')
                # 评论内容
                text = i.xpath('.//div[@class="WB_text"]')
                text = [i.xpath('string(.)') for i in text]
                # 头像地址
                pic_url = i.xpath('.//div[@class="WB_face W_fl"]/a/img/@src')
                print(len(nick_name),len(text),len(pic_url))
                write_comment([i.strip() for i in text], pic_url, nick_name) 

其中写入文件的函数和下载图片的函数如下:

# 下载图片
def download_pic(url, nick_name):
    if not url:
        return
    if not os.path.exists(pic_file_path):
        os.mkdir(pic_file_path)
    resp = requests.get(url)
    if resp.status_code == 200:
        with open(pic_file_path + f'/{nick_name}.jpg', 'wb') as f:
            f.write(resp.content)

# 写入留言内容
def write_comment(comment, pic_url, nick_name):
    f = open('comment.txt', 'a', encoding='utf-8')
    for index, i in enumerate(comment):
        if ':' not in i and '回复' not in i and i != '':
            # 去除评论的评论
            w_comment = i.strip().replace(':', '').replace('\n', '')
            # 写入评论
            f.write(w_comment.replace('等人', '').replace('图片评论', '')+'\n')
            # 获得头像
            download_pic(pic_url[index], nick_name[index]) 

以上就是我们所用到的代码。在公众号后台回复 微博评论爬虫 即可下载完整源代码(附手机版爬虫)。

3.定时爬虫

尽管如此,我们得到的数据还是不够,PC版的微博评论页面也仅仅支持爬到第五十页,第五十一页后就拿不到数据了,如图:

不过,走饭这个微博真的很多人回复,一天的数据就差不多50页了,我们可以通过每天定时爬50页来获取数据。linux系统可以使用crontab定时脚本实现,windows系统可以通过计划任务实现,这里讲讲crontab实现方法。

假设你的Python存放在/usr/bin/且将脚本命名为weibo.py 存放在home中,在终端输入crontab -e后,在最后面增加上这一条语句即可:

0 0 * * * /usr/bin/python /home/weibo.py

在公众号后台回复 微博评论爬虫 即可下载本文完整源代码(附手机版爬虫) 。

如果你喜欢今天的Python 教程,请持续关注Python实用宝典,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们会耐心解答的!

​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 百度指数突变值检测

百度指数是一款非常好用的工具,通过它我们能知道某些关键词在过去的一些日子里的热度变化趋势并能够对这些数据进行分析。如果能用得好百度指数,我们将能产出巨大的价值。

你可以通过关注文章下方的公众号(Python实用宝典),回复 百度指数突变点 获得本文所有的源代码。

今天的教程主要是来教大家如何找出百度指数中突变值的位置,如图所示画框框的部分:

30天数据 突变区域

30天的数据流中很容易通过人工的方法找到突变数据的位置,但如果是180天呢?这可就不好通过人工的方法找了:

180天数据

如何使用Python自动找出这180天里的突变点呢?由于这里涉及到了对时间序列的突变点的检测,我们可以使用一种叫 Pettitt突变点检测 算法

1.获取数据

通过开发者工具找到数据接口,结果发现其接口返回来的数据进行了加密:

看起来就很像字符串替换,如果要从头开始解密的话需要做一些对比工作(把源数据和该加密数据放一起进行比较)或者直接看前端源代码 。由于这里不是今天要讲的重点内容,我直接使用了他人的开源项目: 百度指数爬虫 。你可以通过关注文章最下方的公众号(Python实用宝典),回复 百度指数突变点 获得本文所有的源代码。

将爬取到的数据,按照关键词存放到数组中,你可以很轻易地修改我的代码增加/减少关键词。如下:

from get_index import BaiduIndex

if __name__ == "__main__":
    keywords = ['区块链']
    results = {'区块链':[]}
    baidu_index = BaiduIndex(keywords, '2019-05-04', '2019-11-04')
    for index in baidu_index.get_index():
        if index['type'] == 'all':
            results[index['keyword']].append(index['index'])
    print(results)

2.突变点算法

Pettitt突变点检测 算法是用R语言写的,实现其实很简单。作者并没有说为什么这么做,而是给了几个数学公式,我们只需要跟着做即可。

代码如下:

def pettitt(data):
    data = np.array(data)
    n = data.shape[0]
    k = range(n)
    dataT = pd.Series(data)
    r = dataT.rank()
    Uk = [2*np.sum(r[0:x])-x*(n + 1) for x in k]
    Uabs = list(np.abs(Uk))
    U = np.max(Uabs)
    K = Uabs.index(U)
    p = 2 * np.exp((-6 * (U**2))/(n**3 + n**2))
    if p <= 0.5:
        # 显著
        result = 'yes'
    else:
        # 不显著
        result = 'no'
    return K, result 

我们只需要将数据放入该函数,就能得到这段数据的突变点(一个),由于它只能找出一段数据里的一个突变点,而我们需要获得的是多个突变点,因此还得设置一个移动窗口,获得每个窗口中的突变位置。

3.设置窗口获得每个窗口的突变位置

将数据设为30天一个窗口,检测每个窗口中的突变值:

    length = len(results['区块链'])
    locations = []
    for i in range(0, length, 1):
        pos, result = pettitt(results['区块链'][i:i+29])
        if result == 'yes':
            locations.append(pos+i)
    print(set(locations)) 

结果如下:

这样看实在是不好看出什么,用matplotlib可视化一下:

    print(results)
    plt.plot(range(len(results['区块链'])), [int(i) for i in results['区块链']])
    for i in locations:
        plt.plot(i,int(results['区块链'][i]),'ks')
    my_y_ticks = np.arange(0, 250000, 50000)
    #显示范围为0至25000,每5000显示一刻度
    plt.yticks(my_y_ticks)
    plt.show()

结果:

说实话,不太满意这个结果,有两个比较明显的两个突变点竟然没找出来。除开这两个突变点不说,整体上看,这个检测方法的效果还可以。

文章到此就结束啦, 你可以通过关注文章下方的公众号(Python实用宝典),回复 百度指数突变点 获得本文所有的源代码。

如果你喜欢今天的Python 教程,请持续关注Python实用宝典,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们会耐心解答的!

​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

[准确率:97%] Python 朴素贝叶斯自动分类食品安全新闻

模型成品在极致安食网:https://jizhianshi.com, 木有太多时间维护。

教程源代码点击阅读原文或访问:

https://github.com/Ckend/NLP_DeepLearning_CN_Tutorial

利用朴素贝叶斯来分类食品安全新闻(标题)这种短文本其实精确度并不高,在实际的生产中,由于食品安全和非食品安全的数量差异,我们会发现1000条新闻中可能才出现2条食品安全新闻,也就是说,即便你的模型准确率为95%,1000条新闻中依然会有许多新闻 (50条) 会被错分类,这是一个非常糟糕的结果。因此在生产环境中,如果模型准确率不能高达99%,甚至都无法使用。在我们的研究下,使用朴素贝叶斯,我们的准确率能提高到97%,使用改进的朴素贝叶斯准确率能达到98%以上,使用字符级的卷积神经网络甚至能达到99%,后续的教程我会介绍这两种方法。

为了后续的教程,我们还是先利用最简单的朴素贝叶斯来理解“训练”这个概念。为了尽量简化教程的难度,我尽量不使用数学公式进行讲解,更多的以自然语言和Python代码进行分析。在这个教程中,你所需要的东西有:

1. python 3

2. jieba 分词

3. numpy

4. sklearn (用joiblib保存模型)

训练

朴素贝叶斯的训练,其实就是遍历整个训练集,算出每个词语在不同的分类下出现的概率(该词语/该分类总词数)。最后得到两个向量,这两个向量分别代表了每个词语在食品安全新闻和非食品安全新闻中出现的概率。

for i in range(numTrainNews):

    if classVector[i] == 1:
        p1Num += trainMatrix[i]
        # 每条食品安全新闻中,每个词的数量分布

        p1Sum += sum(trainMatrix[i])
        # 求每条新闻出现的食品安全集合中的词语的数量总合

    else:
        p0Num += trainMatrix[i]
        p0Sum += sum(trainMatrix[i])

p1Vect = log(p1Num / p1Sum)
# 在1的情况下每个词语出现的概率

p0Vect = log(p0Num / p0Sum)
# 在0的情况下每个词语出现的概率

#保存结果
self.p0V = p0Vect
self.p1V = p1Vect

如代码所示,numTrainNews是所有新闻的个数,trainMartix 是新闻进行分词,并数字化后的矩阵(先不用在意这里的数据结构,后面会介绍),其中的[i]代表是第几条新闻,classVector与trainMatrix相对应,它是这些新闻的正确分类(1或0,本例中,1代表食品安全新闻,0代表非食品安全新闻)。

当该新闻是食品安全新闻的时候,即classVector[1] == 1,把它所有词语的频数加到p1Num中, p1Num也是一个向量,保存了所有出现词语的频数。p1Sum是所有词语出现的次数总和。同理,当该新闻不是食品安全新闻的时候,也是使用同样的方式处理。

训练结束后,会得到两个总的概率向量,即p1V和p0V,这两个向量分别代表了每个词语在食品安全新闻和非食品安全新闻中出现的概率。

这里需要注意的是,我们最后对p1V和p0V取对数( np.log( ) )是为了在后续的分类中不会出现许多很小的数(试想一个词在训练集中只出现过1次,但是总词数却有几千个)相乘,这些很小的数相乘可能会导致最后结果被四舍五入为0。取对数能让y值从[0,1]被映射到[-∞, 0],从而避免出现相乘结果被四舍五入的情况。

分类

def classify_vector(self, vec2Classify):
    """
    分类函数,对输入词向量分类
    :param vec2Classify: 欲分类的词向量
    """

    vec2Classify = vec2Classify[0]
    p1 = sum(vec2Classify * self.p1V) + log(self.p1)
    # 元素相乘
    p0 = sum(vec2Classify * self.p0V) + log(1.0 - self.p1)

    if p1 - p0 > 0:
        return 1
    else:
        return 0

vec2Classify 是需要分类的新闻进行分词操作后每个词语在总词表中出现的次数,没有出现的则为0,有出现的为1。以”江西公布食品抽检情况 样品总体合格率95.67%”为例,它分词后的结果是:[‘江西’, ‘公布’, ‘食品’, ‘抽检’, ‘情况’, ‘ ‘, ‘样品’, ‘总体’, ‘合格率’, ‘95.67%’],转换成向量则如下图所示:

其中,像“你们”、“水果”、“购物”这样的词语是不在这条新闻里的,但是它在训练集的其他新闻里,因此总表中会出现它,但是在该新闻的向量中,它会被标记为0. 代表没有出现。

这里还需要注意的是p1这个变量,这个变量叫作“先验概率”,在我们的例子中就是:“现实生活中,多少条新闻里出现一条食品安全新闻”,根据统计,这个数值大概是1/125, 但是我偏向于保守一点的值,因此将其固定设置为1/200. 请注意,我们这里是手动设置先验概率,在scikit-learn等机器学习框架中,他们会根据训练集自动得到这个值,最典型的就是 MultinomialNB 和 GaussianNB,在我们的例子中实际上提升不了多少准确率,因此在这里不讨论。

将当前的词向量和我们刚获得的词向量相乘后,对每个词语得到的值求和,再乘以先验概率就是我们在每个分类上的结果,若该新闻在食品安全的结果大于非食品安全,则被分类为食品安全新闻。

将上面的训练和分类转换成数学公式来进行讲解,那就是这样的:

A:我们需要测试的词语  

B:来自食品安全相关新闻/不是来自食品安全相关新闻的概率。

判断一个单词分别归属两类的概率:

判断一篇新闻是否是食品安全相关的概率:

难怪数学家喜欢写公式,确实比文字简洁很多。


在下面开始讲预处理和模型之前,我需要先把我们的trainNB.py放在这里,好让大家在阅读的过程中回来翻阅,理解预处理和模型中得到的变量是用来干嘛的,以便更好地明白这些代码:

import bayes

from data_helpers import *

from sklearn.externals import joblib

posFile = "./data/train_food.txt"
negFile = "./data/train_notfood.txt"

print("正在获取训练矩阵及其分类向量")
trainList,classVec = loadTrainDataset(posFile,negFile)

print("正在将训练矩阵分词,并生成词表")
vectorized, vocabulary = jieba_cut_and_save_file(trainList,True)

bayes = bayes.oldNB(vocabulary)

# 初始化模型
print("正在训练模型")
bayes.train(vectorized,classVec)

# 训练
print("保存模型")
joblib.dump(bayes, "./arguments/train_model.m")

预处理

讲完训练和测试,你基本上已经知道朴素贝叶斯的运作方式了,接下来我们讲一下如何进行预处理,并从文本中构建词向量和总词表。以下函数都位于GitHub库(见文章首行)的data_helpers.py文件中。

1. 读取训练集

首先从文件中读入我们的训练数据,在我上传的GitHub库(见文章首行)中,我们的训练数据被存在1.NaiveBayes/data/中。

def loadTrainDataset(posFile,negFile):

    """

    便利函数,加载训练数据集

    :param pos: 多少条食品安全相关新闻

    :param neg: 多少条非食品安全相关新闻

    """

    trainingList = []  # 训练集
    classVec = []  # 分类向量
    # 录入食品安全相关的训练集
    posList = list(open(posFile, 'r').readlines())
    posVec = [1] * len(posList)
    trainingList += posList
    classVec += posVec

    # 录入非食品安全相关的训练集
    negList = list(open(negFile, 'r').readlines())
    negVec = [0] * len(negList)
    trainingList += negList
    classVec += negVec

    return trainingList, classVec

2.对新闻进行分词

我们需要对这些新闻进行分词,为了节省以后训练使用的时间,我们还要把分词的结果保存,同样地,这些结果会被保存到data中,名字为cleaned_trainMatrix.txt. 如下所示:

def jieba_cut_and_save_file(inputList, output_cleaned_file=False):

    """

    1. 读取中文文件并分词句子

    2. 可以将分词后的结果保存到文件

    3. 如果已经存在经过分词的数据文件则直接加载

    """

    output_file = os.path.join('./data/', 'cleaned_' + 'trainMatrix.txt')

    if os.path.exists(output_file):
        lines = list(open(output_file, 'r').readlines())
        lines = [line.strip('\n').split(' ') for line in lines]

    else:
        lines = [list(jieba.cut(clean_str(line))) for line in inputList]
        # 将句子进行clean_str处理后进行结巴分词
        lines = [[word for word in line if word != ' '] for line in lines]

    if output_cleaned_file:
        with open(output_file, 'w') as f:
            for line in lines:
                f.write(" ".join(line) + '\n')

    vocabulary = createVocabList(lines)
    # 根据词典生成词向量化器,并进行词向量化
    setOfWords2Vec = setOfWords2VecFactory(vocabulary)
    vectorized = [setOfWords2Vec(news) for news in lines]

    return vectorized, vocabulary

def clean_str(string):
    """
    1. 将除汉字外的字符转为一个空格
    2. 除去句子前后的空格字符
    """

    string = re.sub(r'[^\u4e00-\u9fff]', ' ', string)
    string = re.sub(r'\s{2,}', ' ', string)
    return string.strip()

下面介绍其中出现的createVocabList等函数。

3. 生成词典

还记得我们前面讲到的总词表吗,它记录了所有单词,它包含了所有新闻中出现的,但是不重复的词语。

def createVocabList(news_list):
    """
    从分词后的新闻列表中构造词典
    """
    # 创造一个包含所有新闻中出现的不重复词的列表。
    vocabSet = set([])
    for news in news_list:
        vocabSet = vocabSet | set(news)
        # |取并

    return list(vocabSet)

4.将分词后的新闻向量化

def setOfWords2VecFactory(vocabList):
    """
    通过给定词典,构造该词典对应的setOfWords2Vec
    """

    #优化:通过事先构造词语到索引的哈希表,加快转化
    index_map = {}
    for i, word in enumerate(vocabList):
        index_map[word] = i

    def setOfWords2Vec(news):
        """
        以在构造时提供的词典为基准词典向量化一条新闻
        """

        result = [0]*len(vocabList)
        for word in news:
            #通过默认值查询同时起到获得索引与判断有无的作用
            index = index_map.get(word, None)

            if index:
                result[index] = 1

        return result

    return setOfWords2Vec

5.最后是测试需要用到的向量化新闻

原理和前边对训练集的向量化相似,但是由于是测试时对单个新闻的向量化,因此我们把它分开了。

def vectorize_newslist(news_list, vocabulary):
    """
    将新闻列表新闻向量化,变成词向量矩阵
    注:如果没有词典,默认值为从集合中创造
    """

    # 分词与过滤

    cut_news_list = [list(jieba.cut(clean_str(news))) for news in news_list]

    # 根据词典生成词向量化器,并进行词向量化
    setOfWords2Vec = setOfWords2VecFactory(vocabulary)
    vectorized = [setOfWords2Vec(news) for news in cut_news_list]

    return vectorized, vocabulary

贝叶斯模型(bayes.py)

为了方便保存模型,我们将模型视为一个类,类的初始化如下。

def __init__(self, vocabulary):
    self.p1V = None
    self.p0V = None
    self.p1 = None
    self.vocabulary = vocabulary

其中,vocabulary是在运行完jieba_cut_and_save_file后获得的。在模型初始化的时候我们需要传入vocabulary,见前面的trainNB.py.

1.训练函数

在前面讲解训练的时候我曾经用到这里的一部分代码,下面是全部代码,其实要讲的都已经讲过了,一些细分方面的东西都已经写在注释中了。

def train(self, trainMatrix, classVector):
    """
    训练函数
    :param trainMatrix: 训练词向量矩阵
    :param classVector: 分类向量
    """

    numTrainNews = len(trainMatrix)
    # 多少条新闻

    numWords = len(trainMatrix[0])
    # 训练集一共多少个词语

    # pFood = sum(classVector) / float(numTrainNews)
    # 新闻属于食品安全类的概率
    pFood = float(1)/float(200)

    p0Num = ones(numWords)
    p1Num = ones(numWords)
    p0Sum = 2.0
    p1Sum = 2.0
    # 以上初始化概率,避免有零的存在使后面乘积结果为0

    # self.words_weight

    for i in range(numTrainNews):
        if classVector[i] == 1:
            p1Num += trainMatrix[i]
            # 每条食品安全新闻中,每个词的数量分布
            p1Sum += sum(trainMatrix[i])
            # 求每条新闻出现的食品安全集合中的词语的数量总合

        else:
            p0Num += trainMatrix[i]
            p0Sum += sum(trainMatrix[i])

    p1Vect = log(p1Num / p1Sum)
    # 在1的情况下每个词语出现的概率

    p0Vect = log(p0Num / p0Sum)
    # 在0的情况下每个词语出现的概率

    #保存结果

    self.p0V = p0Vect
    self.p1V = p1Vect
    self.p1 = pFood


2. 分类新闻

测试新闻的时候我们需要用到以下两个函数,一个是用来将新闻词向量化,一个是用来将词向量进行最后的分类操作。

def classify_news(self, news):
    """
    分类函数,对输入新闻进行处理,然后分类
    :param vec2Classify: 欲分类的新闻
    """

    vectorized, vocabulary = vectorize_newslist([news],self.vocabulary)

    return self.classify_vector(vectorized)

def classify_vector(self, vec2Classify):
    """
    分类函数,对输入词向量分类
    :param vec2Classify: 欲分类的词向量
    """

    vec2Classify = vec2Classify[0]
    p1 = sum(vec2Classify * self.p1V) + log(self.p1)
    # 元素相乘

    p0 = sum(vec2Classify * self.p0V) + log(1.0 - self.p1)

    if p1 - p0 > 0:
        return 1
    else:
        return 0

这样我们便完成了训练时所需要的所有函数,接下来尝试训练,在该文件夹中运行python trainNB.py. 训练并保存模型:

成功!接下来我们来测试这个模型:

import bayes
from data_helpers import *
from sklearn.externals import joblib

posFile = "./data/eval_food.txt"
negFile = "./data/eval_notfood.txt"

print("正在得到测试矩阵及其分类向量")
trainList,classVec = loadTrainDataset(posFile,negFile)
nb = joblib.load("arguments/train_model.m")

# 读取模型
results = []
for i in trainList:
    result = nb.classify_news(i)
    results.append(result)

# 测试模型
acc = 0.0
correct = 0
for i in range(len(classVec)):
    if results[i] == classVec[i]:
        correct += 1

acc = correct/len(classVec)

print("正确率为:"+str(acc))

正确率为97.7%.

这只是第一篇教程,后续我们还有办法对食品安全新闻更加精确地进行分类,喜欢大家喜欢,有问题可以在评论区进行讨论。