python 中使用装饰器来统一检查 flask 用户权限

最近在一个项目中,需要判断 restful 接口函数传入的时候,是否之前已经登录状态是某个特定用户,以及该用户有没有指定的权限。检查下来如果没有的话,立刻返回错误,中断功能。

遮掩的场景虽然也可以通过标准的调用函数来操作,但都不如用装饰器来得简单。都知道装饰器好用不好写,废话不说,先来看看这个场景怎么实现,还是有一定的通用性的。

def validate_current_is_admin(f):
    @functools.wraps(f)
    def decorated_function(*args, **kws):
        # 需要在登录状态调用, 检查是否为有admin权限的用户登录,
        # 如果不是,返回错误码;
        if g.user.user_name != 'admin':
            raise CustomFlaskErr(USER_MUST_HAS_ADMIN_PRIVILEGE, status_code=401)

        # 验证权限是否为 admin, 不是的话,返回401错误
        if g.user.role_id != Permission.ADMIN:
            raise CustomFlaskErr(USER_MUST_HAS_ADMIN_PRIVILEGE, status_code=401)

        return f(*args, **kws)

    return decorated_function

 

这是一个标准的装饰器的写法,如果你要写一个简单的装饰器,整个框架可以参考。

装饰器调用举例:

@app.route('/api/create_user', methods=['POST'])
@auth.login_required
@validate_current_is_admin
def create_user():

    # 获得参数
    user_name = request.json.get('user_name')
    password = request.json.get('password')
......

 

核心代码的业务逻辑也不复杂,根据 flask 的 g 对象中预存的用户 user 进行检查处理,flask 的这些定义非常灵活,flask.g 怎么使用可以查看 flask 的文档。

这里的user以及相关的属性属于具体业务逻辑,就不展开解释了,可以望文生义。

如果检查下来不符合的话,会调用自定义的 flask 错误,这部分内容可以查看之前写的 python flask 写 api 如何返回自定义错误

因为不对args和kws这些参数进行解析和处理,所处理的是 flask 全局对象。最后将参数都原路打包返回即可,没有问题的话交给使用装饰器的代码继续处理。

这个例子比较简单,主要还是熟悉装饰器的基本用法。

(首发简书

python flask 写 api 如何返回自定义错误

在 python 开发中,利用 flask 写 restful api 函数的时候,除了标准的400、500等这些返回码通过 abort() 返回以外,怎么另外返回自定义的错误代码和信息呢?

我们碰到的业务场景是对于api 输入参数的各类校验以及在业务逻辑执行的时候,都会返回统一的400代码,同时也会返回我们约定的描述详细错误的代码以及描述字符串,提供给调用方来处理,这样可以让其用户体验做得更好,同时详细错误代码和描述字符串也会自动打印在 log 日志中。

flask 的官方文档中告诉我们:

默认情况下,错误代码会显示一个黑白的错误页面。如果你要定制错误页面, 可以使用 errorhandler() 装饰器

在写 restful api 的时候,并没有页面可以返回,我们可以在 flask 提供的代码基础上稍加改造如下。

在你的初始化 flask app 的相关代码中加入下面两个函数:

@app.errorhandler(CustomFlaskErr)
def handle_flask_error(error):

    # response 的 json 内容为自定义错误代码和错误信息
    response = jsonify(error.to_dict())

    # response 返回 error 发生时定义的标准错误代码
    response.status_code = error.status_code

    return response

 

class CustomFlaskErr(Exception):

    # 默认的返回码
    status_code = 400

    # 自己定义了一个 return_code,作为更细颗粒度的错误代码
    def __init__(self, return_code=None, status_code=None, payload=None):
        Exception.__init__(self)
        self.return_code = return_code
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    # 构造要返回的错误代码和错误信息的 dict
    def to_dict(self):
        rv = dict(self.payload or ())

        # 增加 dict key: return code
        rv['return_code'] = self.return_code

        # 增加 dict key: message, 具体内容由常量定义文件中通过 return_code 转化而来
        rv['message'] = J_MSG[self.return_code]

        # 日志打印
        logger.warning(J_MSG[self.return_code])

        return rv

 

CustomFlaskErr 是我们自己写的处理错误的类,然后通过 @app.errorhandler(CustomFlaskErr) 这个装饰器在 flask 中注册。

具体功能在注释里基本都写了,我们看一下怎么使用这个自定义错误处理器。

# 用户名输入为空
    if user_name is None:
        raise CustomFlaskErr(USER_NAME_ILLEGAL, status_code=400)

 

当需要处理某个错误的时候,rasie 刚才的 CustomFlaskErr,传递另外定义好的自己的错误代码,以及标准的返回代码;

上面说的常量定义文件可以参考如下:

USER_ALREADY_EXISTS = 20001  # 用户已经存在
J_MSG = {USER_ALREADY_EXISTS: 'user already exists'}

 

通过这样的机制,就做到了在具体 restful api 的业务逻辑代码中简单的进行各类自定义错误的处理,所有的错误处理是集中的,细颗粒度的错误代码和消息也是集中维护,便于扩展。

flask 官方文档和一些网上的资料都说比较简单,实践中摸索了这样的实现方式供参考。

(首发简书

学习 python 的可爱的孩子们

又是一个学期,时间真快,自己也不容易,这个学期14节课,风里来雨里去,来回就要1个多小时,坚持下来。今天终于是最后一节课,欢乐的考试时光。

还是希望现在这些条件越来越好的孩子们,可以好好学习电脑,学习编程,成为未来的栋梁之才!

小时候自己主要在中学和少科站,至今不能忘记当年格致中学的励幼娣、周柏生老师,少科站的曹文浩老师,谢谢他们的悉心指点,当年的我也有点像现在这些孩子,聪明、贪玩。很多道理都是后来才懂。

开源内部培训的 python 教程

这几年,作为布道者,始终在探索一些新的知识点,在 python 以及其应用的 restful api、数据统计、机器学习等领域算是略有斩获吧,也更加理解,作为初学者来说,如今的编程领域的确功能强大,但是门槛其实并不低。

在公司内部做了一段时间 python 培训,所以逐步将这些内部培训材料公开出来,目前是最基本的 python basic,也就是 python 的入门教材。

https://github.com/chinapnr/python_study

拙作,欢迎拍砖。希望之后有能力和时间继续各类 python 专题,特别是机器学习、NLP、flask、pandas 和 numpy 等,都非常有趣和功能强大,python 和 js、java 一样,几乎可以胜任任何领域的开发。

学习 python 这一年

大约2015年4月到5月开始,正式学习 python,发现之前大概在2009年左右也学过,可能当时没有坚持下去。

记得去年开始学 python 的时候,1个月左右,给同事写了一个基于 csv 处理的小程序,当时就被 python 的简洁优雅所倾倒。

然后边学边写东西,本来为公司 zion 框架写一个 dsl,当时水平还不行,所以写成了一个 sql 的包装工具 r2,用来在前端业务逻辑调用后台数据库的时候简化 sql 语句写法以及做到和数据库无关。

真正让我自己 python 水平感觉有点突破的是几个事情:

1 写真正生产上的应用。需要考虑的事情非常多,比如一个简单的 r2,就让我学到了 log、conf、import、class 等很多基本知识的应用,以及单元测试、性能测试。

2 教别人 python。这是让自己的基础知识巩固最好的办法之一。要把别人教会,且处理各类奇怪的运行错误,很有挑战。这样的结果,是对诸如列表、字符串、元组、字典、函数、循环等基本概念非常清楚。不管多复杂的程序,90%的代码其实还是基本的操作,在这90%的代码设计和编写的时候可以减少错误,节约时间,自然有用。

3 看资料。晚上有大量的英文和中文的 python 资料,绝大多数都写的很好,受益匪浅。

在我的推进下,公司也在个别项目上开始使用 python,通过 flask 来构建 restful 接口,机器学习也基于 keras 和 tensorflow 开始了实际的应用。

我相信,进步会是跳跃式的,而这还是仅仅发生在一年间,python 在胶水语言的应用,连接 js、php、java 和 moble 开发上面,以及 python 在人工智能、机器学习上这几年强劲的表现,都让人激动不已。

我的2016 Python B 班

从三月开始,江湖救急。

也一直好为人师。

终于找到了舞台。

昨天,这13个小孩,Python 初级班结束。

谢谢豆妈,谢谢静枫,谢谢所有的家长,谢谢聪明的孩子们。

很多时候感觉回到了自己小时候,在少科站、少年宫、格致老大楼的机房。

IMG_7951

IMG_7952

或许,这样耗费很多业余时间,来回路上,准备讲义和题目,制作考试用的 rpg 游戏,有的人会觉得我很傻,很多时候我也认为自己不是一个一流的程序员,但是当这些孩子们编程入门了,或许明天的 facebook、google 的创造者就在他们中间,想想这也是很开心的。

使用 python 基于朴素贝叶斯进行文本分类学习笔记之五:增加停用词

还记得之前我们搭建的朴素贝叶斯的分类模型,在正面负面各9个样本的情况下,正确率如下。

{'01': 1, '10': 5, '00': 7, '11': 3}
(0.875, 0.375)

 

我开始引入停用词,从测试来看,主要是在训练的时候效果比较明显,因为中文样本中标点符号和很多常用词占比还是比较大的,引入停用词机制前后,差不多是三分之一的量。

下面是读入停用词的函数代码。

 def read_stopwords(self):

        self.stopwords_list = []

        # 获得停用词文件的本地文件
        filename = get_long_filename_with_sub_dir_module('bayes', 'stopwords.txt')[1]

        with open(filename, 'r') as f:
            for line in f:
                self.stopwords_list.append(line.rstrip())

        print(self.stopwords_list)

 

目前我是将朴素贝叶斯的这个分类,作为 fish_base 包的一部分功能。对于停用词来说属于功能部分,而之前的训练文档其实不是功能部分,是由真实代码或者 demo 代码传递给分类器的。因此训练文档和测试文档属于 demo 代码,而停用词属于分类器代码。

(国内网上很多的贝叶斯代码的解释者可能都是学习数据分析、统计分析的职业人士,但是代码的写法和扩展性,略显简单,我可能习惯了尽量做得完善一些,副作用是会比较复杂一点,需要更多 python 和编程的基础)

整个类的代码最后给出。这里解释一下

get_long_filename_with_sub_dir_module

这个函数是为了获得模块运行时候的路径,否则一般的获取路径办法获得是执行文件的路径,这就是之前说的如果只是一个 demo,不会有这个问题,统统在一个路径下面,但是如果考虑做成包,做成 module,封装逻辑的话,就会复杂一点,另外还要考虑各个操作系统可能的差异。

这个函数属于 fish_base 的 commmon 部分,目前 fish_base 公开的版本1.0.7 中还不支持,在 github 上的1.0.8 支持该函数。

停用词来自于之前说到过的 snownlp 包,感谢作者。停用词数量大约1400个左右,以后考虑支持用户自定义停用词。

在训练过程引入停用词之后,再用昨天的测试样本测试一下,得到了如下的结果。

{'00': 8, '10': 4, '01': 0, '11': 4}
(1.0, 0.5)

 

正面被分类成正面准确率100%,负面被分类成负面也从37.5% 提升到 50%,略有进步。

按照公司数据分析团队的建议,后者百分比还不够高是因为训练样本太少的缘故。

如此少的训练样本,得到了这样的结果,不得不说朴素贝叶斯的确很厉害,一点也不 naive。

之后我们还会继续改进的地方是:
1 将判断错误的样本增加到训练样本中,来重新训练
2 分词,增加用户自定义词汇
3 测试材料,增加停用词处理(目前测试下来,对提升正确率没有作用),这也和目前举例中的测试文本比较短有关,并且这个也和性能有关,训练过程中引入停用词判断对性能的损失是无所谓的,反正是在训练过程中,但是实际使用分类还是要越快越好
4 我们会将整个分类器通过 Falsk 包装成 web 接口,加入 job 概念,这样调用程序只需要传入训练文本和测试文本,之后就可以生成特定的分类器概率矩阵,然后供调用程序在真实场景下使用,作为调用程序的项目组不需要知道后面这些细节
5 所有的数据存储可以本地实例化,sqlite 或者 mysql 之类的支持
6 支持三元和四元的分类

从用 python 来实现朴素贝叶斯分类器来对中文文本进行倾向性分析来说,暂时告一段落了。所有代码都是这几天里面改写和增加的,防止错误的部分几乎没有,也没有进行完整的单元测试。

下面是当前版本完整的朴素贝叶斯分类器的类的代码,直接使用需要有 python 基础,也可以之后使用 fish_base 包来调用。

最新版本源代码在这里: github.com/chinapnr/fish_base

pypi 的安装包会稍微滞后一些。

from numpy import *
import jieba

from fish_base import get_long_filename_with_sub_dir_module


class ClassNaiveBayes:

    # 训练 list, 默认内容, 原书中的内容
    train_doc_list = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                      ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                      ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                      ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                      ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                      ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    # 倾向性向量, 0:正面 1:负面
    train_doc_sent_vec = [0, 1, 0, 1, 0, 1]

    # 单词列表集合
    word_list = []

    # 正面和负面概率, 先验概率
    p0_v = 0
    p1_v = 0
    p_ab = 0

    stopwords_list = []

    # 2016.5.18
    # 读入停用词
    def read_stopwords(self):

        self.stopwords_list = []

        # 获得停用词文件的本地文件
        filename = get_long_filename_with_sub_dir_module('bayes', 'stopwords.txt')[1]

        with open(filename, 'r') as f:
            for line in f:
                self.stopwords_list.append(line.rstrip())

        print(self.stopwords_list)

    # 2016.5.19
    def word_optimize(self, l):
        # 停用词过滤
        temp_word_list = [x for x in l if x not in self.stopwords_list]
        return temp_word_list

    # 2016.5.16 5.19
    # 创建单词集合
    # 输入 data_list: 数据列表内容, 两维list
    # 输出 单维list
    def create_word_list(self, data_list):
        # create empty set
        word_set = set([])
        for document in data_list:
            # union of the two sets
            word_set = word_set | set(document)
        word_list = list(word_set)
        # 词汇处理
        word_list = self.word_optimize(word_list)
        # print('word list count:', len(word_list))
        return word_list

    # 2016.5.16
    # 将单词 list 转换为向量
    # 输入 word_list: 单词列表 new_word_list: 需要向量化的单词列表
    # 输出 vec: 生成的向量数组
    @staticmethod
    def words_to_vec(word_list, new_word_list):
        vec = [0] * len(word_list)
        for word in new_word_list:
            if word in word_list:
                vec[word_list.index(word)] += 1
            else:
                pass
                # print("the word: %s is not in my Vocabulary!" % word)
        return vec

    # 2016.5.16
    # 进行 naive bayes 训练
    # 输入 train_matrix: 训练举证 train_category: 正反向量列表
    # 输出 p0_v, p1_v:正面反面概率, p_ab:先验概率
    @staticmethod
    def train_nb0(train_matrix, train_category):

        num_train_docs = len(train_matrix)
        num_words = len(train_matrix[0])
        p_ab = sum(train_category) / float(num_train_docs)

        # 创建给定长度的填满1的数组
        p0_num = ones(num_words)
        p1_num = ones(num_words)

        p0_d = 2.0
        p1_d = 2.0
        for i in range(num_train_docs):
            if train_category[i] == 1:
                p1_num += train_matrix[i]
                p1_d += sum(train_matrix[i])
            else:
                p0_num += train_matrix[i]
                p0_d += sum(train_matrix[i])

        p1_v = log(p1_num / p1_d)
        p0_v = log(p0_num / p0_d)
        return p0_v, p1_v, p_ab

    # 2016.5.16
    # 分类
    # 输入 向量, 正面反面概率,事件概率
    # 输出 正面或者反面
    @staticmethod
    def classify_nb(vec, p0_vec, p1_vec, p_class1):
        # element-wise mult
        p0 = sum(vec * p0_vec) + log(1.0 - p_class1)
        p1 = sum(vec * p1_vec) + log(p_class1)
        print('p0:', p0, 'p1:', p1)
        if p1 > p0:
            return 1
        else:
            return 0

    # 2016.5.16
    # 训练, 生成需要的向量参数等
    def train(self):
        # 生成单词列表集合
        self.word_list = self.create_word_list(self.train_doc_list)

        # 训练矩阵初始化
        train_matrix = []

        # 根据训练文档进行循环
        for post_in_doc in self.train_doc_list:
            # 构建训练矩阵, 将单词列表转化为向量
            train_matrix.append(self.words_to_vec(self.word_list, post_in_doc))

        # 根据训练矩阵和情感分析向量进行训练,得到
        self.p0_v, self.p1_v, self.p_ab = self.train_nb0(array(train_matrix), array(self.train_doc_sent_vec))

    # 2016.5.16
    # 根据输入内容测试 Naive Bayes 模型
    # 输入 test_word_list: 需要测试的单词 list
    # 输出 0 or 1, 表示正面或者反面
    def run_nb(self, word_list):

        # 对输入的内容转化为向量
        this_post_vec = array(self.words_to_vec(self.word_list, word_list))

        # 返回分类的值
        return self.classify_nb(this_post_vec, self.p0_v, self.p1_v, self.p_ab)

    # 2016.5.18
    @staticmethod
    def init_cut_word():
        jieba.initialize()

    # 2016.5.18
    # 打开训练文档, 将内容增加到内部变量
    def open_train_doc_ch(self, filename, class_mark):

        train_txt0 = []
        with open(filename, 'r') as f:
            for line in f:
                train_txt0.append(line.rstrip())

        for item in train_txt0:
            s = list(jieba.cut(item))
            self.train_doc_list.append(s)
            self.train_doc_sent_vec.append(class_mark)

    # 2016.5.18
    # 根据测试样本来测试分类的准确率
    # 输出 人工正确/机器判断正确 人工错误/机器判断错误 的两个百分比
    def test_nb(self, filename):

        test_doc_list = []
        pre_class_list = []

        # 设定测试结果 dict
        test_result_dict = {'11': 0, '10': 0, '00': 0, '01': 0}

        # 打开测试文本
        with open(filename, 'r') as f:
            for line in f:
                # 获得人工设定的类别
                pre_class_list.append(line[0:1])
                # 获得需要测试的文本
                test_doc_list.append(line[2:].rstrip())

        # 测试文本的长度
        test_doc_count = len(test_doc_list)

        for i, item in enumerate(test_doc_list):

            s = list(jieba.cut(item))

            # 对输入单词进行优化处理
            s = self.word_optimize(s)

            # 获得程序分类结果
            computer_class = self.run_nb(s)
            # 获得人工设定的分类结果
            pre_class = pre_class_list[i]

            # 结果记录到测试结果 dict 中
            index = '**'

            if pre_class == '1':
                index = str(10 + computer_class)
            if pre_class == '0':
                index = '0' + str(computer_class)

            test_result_dict[index] += 1

            print(s, pre_class, computer_class)

        print(test_result_dict)

        # 返回结果, 假设测试文本中正面和负面各占一半
        return test_result_dict['00'] / (test_doc_count / 2), test_result_dict['11'] / (test_doc_count / 2)

 

下面是测试用的程序。

from fish_base import bayes
import jieba

nb = bayes.ClassNaiveBayes()

nb.train()

test_list = ['love', 'my', 'dalmation']
print(test_list, 'classified as: ', nb.run_nb(test_list))

test_list = ['stupid', 'garbage']
print(test_list, 'classified as: ', nb.run_nb(test_list))

nb.train_doc_list = []
nb.train_doc_sent_vec = []

# for Chinese

# 初始化分词
nb.init_cut_word()

# 读入停用词
nb.read_stopwords()

nb.open_train_doc_ch('train_bayes/good.txt', 0)
nb.open_train_doc_ch('train_bayes/bad.txt', 1)

nb.train()
print(nb.p0_v, nb.p1_v, nb.p_ab)

print(nb.test_nb('train_bayes/test.txt'))

# 指定测试
print()
test_s = '这个手机很好,我很喜欢'
print(test_s)
test_list = list(jieba.cut(test_s))
p = nb.run_nb(test_list)
if p == 0:
    print('classified as good ')
else:
    print('classified as bad ')

# while 1:
#     test_s = input('input comment:')
#     test_list = list(jieba.cut(test_s))
#     print(test_list)
#     p = nb.run_nb(test_list)
#     if p == 0:
#         print('classified as good ')
#     else:
#         print('classified as bad ')
#
#     print()

# 2016.5.18
# {'01': 1, '10': 5, '00': 7, '11': 3}
# (0.875, 0.375)
#
#
# 2016.5.18 增加去除停用词,一般意义
# {'11': 4, '00': 8, '10': 4, '01': 0}
# (1.0, 0.5)

 

(感谢这几天项目开发团队和数据分析开发团队同事支持和耐心讲解。)

使用 python 基于朴素贝叶斯进行文本分类学习笔记之一:开始
使用 python 基于朴素贝叶斯进行文本分类学习笔记之二:将原书程序修改并转换为类
使用 python 基于朴素贝叶斯进行文本分类学习笔记之三:中文内容的倾向性判断初步
使用 python 基于朴素贝叶斯进行文本分类学习笔记之四:增加测试文本和计算正确率
使用 python 基于朴素贝叶斯进行文本分类学习笔记之五:增加停用词

使用 python 基于朴素贝叶斯进行文本分类学习笔记之四:增加测试文本和计算正确率

当前的代码可以通过下面的链接下载,我在 github 上更新的可能比较快,所以下面给出代码历史版本:

bayes.py: https://github.com/chinapnr/fish_base/blob/55a34478425d0395a0b09fe7fce5c30b3f8cd966/fish_base/bayes/bayes.py

bayes_test.py
https://github.com/chinapnr/fish_base/blob/55a34478425d0395a0b09fe7fce5c30b3f8cd966/demo/bayes_test.py

最新版的代码可以在这里找到: https://github.com/chinapnr/fish_base, demo 在 demo 路径下。我把 naive bayes 代码合并到我们的 fish_base 库中了。

除了正面和负面文本训练以外,现在增加了测试文本,并且计算正确率,这样改进算法、改进训练之后的结果就可以通过正确率进行比较了。所以在优化停用词、中文标点前,先增加了这个功能,代码如下。

 # 2016.5.18
    # 根据测试样本来测试分类的准确率
    # 输出 人工正确/机器判断正确 人工错误/机器判断错误 的两个百分比
    def test_nb(self, filename):

        test_doc_list = []
        pre_class_list = []

        # 设定测试结果 dict
        test_result_dict = {'11': 0, '10': 0, '00': 0, '01': 0}

        # 打开测试文本
        with open(filename, 'r') as f:
            for line in f:
                # 获得人工设定的类别
                pre_class_list.append(line[0:1])
                # 获得需要测试的文本
                test_doc_list.append(line[2:])

        # 测试文本的长度
        test_doc_count = len(test_doc_list)

        for i, item in enumerate(test_doc_list):

            s = list(jieba.cut(item))

            # 获得程序分类结果
            computer_class = self.run_nb(s)
            # 获得人工设定的分类结果
            pre_class = pre_class_list[i]

            # 结果记录到测试结果 dict 中
            index = '**'

            if pre_class == '1':
                index = str(10 + computer_class)
            if pre_class == '0':
                index = '0' + str(computer_class)

            test_result_dict[index] += 1

            print(s, pre_class, computer_class)

        print(test_result_dict)

        # 返回结果, 假设测试文本中正面和负面各占一半
        return test_result_dict['00'] / (test_doc_count / 2), test_result_dict['11'] / (test_doc_count / 2)

 

测试代码也做了修改,更加简化了,先进行中文分词初始化,然后训练,然后测试,就可以了。

nb.init_cut_word()

nb.open_train_doc_ch('train_bayes/good.txt', 0)
nb.open_train_doc_ch('train_bayes/bad.txt', 1)

nb.train()
print(nb.p0_v, nb.p1_v, nb.p_ab)

print(nb.test_nb('train_bayes/test.txt'))

 

测试文本举例如下。

0|这手机真不错
0|大屏幕,我很喜欢
0|速度快,质量好
0|买了给妈妈的,快递很快,服务好
0|挺喜欢这个手机的
0|手机屏幕大,挺好的
0|手机挺好的
0|很喜欢这个手机
1|破手机
1|什么破机器,速度那么慢
1|手机很一般,速度慢
1|不太喜欢
1|不能开机,已经保修
1|屏幕上有划痕,不开心
1|不相信国产手机了,质量一般,做工粗糙
1|已经退货了

 

第一个数字是标注这一行内容是正面还是负面,然后程序通过比较运行分类器的结果和人工预先设定的这个结果,得到正确率。最重要的自然是应该是正面且判断为正面,应该是负面且判断为负面这两个比例。

{'01': 1, '10': 5, '00': 7, '11': 3}
(0.875, 0.375)

 

我用 dict 来记录应该是正面负面和判断为正面负面(分别用0和1)四个值
之后的比例,就是前面说的两个比例,从目前的情况来看,对于正面的判断比较准确,对于负面的判断还在50% 以下,之后需要继续提高,先从停用词开始。

使用 python 基于朴素贝叶斯进行文本分类学习笔记之一:开始
使用 python 基于朴素贝叶斯进行文本分类学习笔记之二:将原书程序修改并转换为类
使用 python 基于朴素贝叶斯进行文本分类学习笔记之三:中文内容的倾向性判断初步
使用 python 基于朴素贝叶斯进行文本分类学习笔记之四:增加测试文本和计算正确率
使用 python 基于朴素贝叶斯进行文本分类学习笔记之五:增加停用词

使用 python 基于朴素贝叶斯进行文本分类学习笔记之三:中文内容的倾向性判断初步

在之前程序的基础上,我尝试对中文内容进行倾向性判断,到目前为止我做的都是二元的,非黑即白,我的同事告诉我朴素贝叶斯也完全可以用在多类别的分类上,之后有机会再找应用场景测试一下,目前我们在实际中的应用场景来说,二元的暂时够用了。

中文在文本分类上和英文最大的不同首先要解决分词问题,还好有很多前辈为之付出了很多努力,在 python 下我使用 jieba 来进行中文的分词,https://github.com/fxsjy/jieba,另外,这个 https://github.com/isnowfy/snownlp 看上去也不错,并且还内置了注入倾向性分析等功能,没有使用过,不知道准确率和性能如何。

之前在class 中训练文本是作为类变量的,因此申明类的实例后,可以自己定义训练文本,我们将中文文本分别放在一个 good.txt 文件和一个 bad.txt 文件,然后通过下面的代码读入训练文本与预先设定的分类。

jieba.initialize()

nb.train_doc_list = []
nb.train_doc_sent_vec = []

with open('train/good.txt', 'r') as f:
    train_txt0 = []
    for line in f:
        train_txt0.append(line)

for item in train_txt0:
    s = list(jieba.cut(item))
    nb.train_doc_list.append(s)
    nb.train_doc_sent_vec.append(0)

with open('train/bad.txt', 'r') as f:
    train_txt1 = []
    for line in f:
        train_txt1.append(line)

for item in train_txt1:
    s = list(jieba.cut(item))
    nb.train_doc_list.append(s)
    nb.train_doc_sent_vec.append(1)

nb.train()
print(nb.p0_v, nb.p1_v, nb.p_ab)

 

运行正常的话,可以得到 p0 和 p1 两个向量矩阵,以及先验概率 0.5。

下面是我用来训练的文本,用的是在京东上随便找的一个手机的好评和差评的第一页内容,各9条,只是测试,这个训练样本以及中文分词还有很大的改进之处,稍后再说。

很不错的手机,手感很好,操作流畅!期待后续表现
用了很久才来评价的,真的很好用。速度很快,海思950真的不错,玩大型游戏也不卡,不发热。专用充电头充电很快,电池也很耐用。看看视频,打电话,上微信,中度用户一天绝对没问题。就是华为预装的软件比较多,要是再简化点就好了。
手机当然是很好的,送货员也是很及时的喽。京东的服务可以说是电商之中最好最及时的。买啥电器产品继续选择京东的了!手机有点大,晚上睡觉不太好用我估计,不沉但是有分量。美腿是我一直用的系列,自己国产的芯片这么牛逼真的要点赞了!!希望华为越来越好,国产不容易,大家都理解。抢购为了更好的排生产计划我们也能理解!!!
很好很强大的手机,京东的速度还是一流的,真的是特别的快。上午下单,下午肯定就能到。这个是其他网站不能比拟的,简直就是快的没道理了。还有就是mate8的指纹怎么那么快呢,一摸就解锁了。这个怎么做到的????这次买mate8 就是赚到了。哈哈,下次给老婆买P9。
帮老弟买的,当时一看这手机就很喜欢,可惜自己已经有水果了,否则也就买这个了
一如既往的好,3079买的。一堆赠品。
东西很好,送的皮套没啥用,其他都挺好
手机不错,老婆喜欢。
很好的手机,媳妇儿非常喜欢

 

用了一个月了,还可以。就是前两天屏幕摔碎了,去换屏幕,拆开后 里面惨目忍睹,机子好像翻新过,零件上的用签字笔写的AB编号到处都是。还是就是这个手机里面的小螺丝,都动过。现在开始质疑网城尤其是京东的产品,心里很不舒服
第一次京东购物,三千大洋买个手机用了不到半个月昨晚花屏了,完全没有预兆出现满屏竖条,有两分钟都不能关机,真心害怕啊,不知道能不能退货,手机现在工作太重要了都不敢再用了
摄像头镜头破了
放第二张卡就不能放扩展卡!为什么没有说明啊!就32g根本就不够用的,不能放卡,这是逼着买贵的啊!差评
昨天收到,今天发现接近屏幕中间横贯屏幕有条绿线,真想骂人
妈的,三网电信4g不可以,只能3g,找客服说要我售后,我才买的新机器就找售后,吃一堑长一智!
价格本来就比其他地方的贵200多,我还在京东买不就是为了放心,省心吗?结果这次大出意料,收到手机后正常出厂带的膜是有个小耳朵好让你撕下来的,结果这个没有就贴着张膜,膜的周边还有好多灰,一看就不是一天形成的,边框耳机孔左边有一个针眼大小的磕碰痕迹,一看什么情况大家心知肚明吧,有点愤怒联系了客服,后来问我朋友,朋友急着用就算了,不换了。但是,这次购物让我对京东有所动摇了,有点寒心,先这样吧,这个差评不是给mate8的,手机无罪,给谁谁明白!!
收到后,手机是坏的,不能发出任何声音,已申请售后,看售后的操作状态再做评价。
这款 手机 我用了16天 电池 就不能用了 花了* 亲 这手机质量令人担忧 我闹心 闹了半个月了 咋回事 * 元 买的 也算是中上等手机了 很无奈 质疑品质 去售后修手机 都要花好几百车费 去了好几趟了 闹心啊

 

然后用下面的代码先收工来测试一下。

while 1:
    test_s = input('input comment:')
    test_list = list(jieba.cut(test_s))
    print(test_list)
    p = nb.testing_nb(test_list)
    if p == 0:
        print('classified as good ')
    else:
        print('classified as bad ')

    print()

 

下面是执行的结果,为了查看分词效果,我把测试文本的分词结果一并显示了出来

input comment:这个手机很好用,我很喜欢
['这个', '手机', '很', '好', '用', ',', '我', '很', '喜欢']
p0: -32.2635265486 p1: -39.1939498346
classified as good 

input comment:这个手机很破,一点也不好用
['这个', '手机', '很破', ',', '一点', '也', '不好', '用']
p0: -17.0641800281 p1: -18.803492389
classified as good 

input comment:这个手机屏幕很大,用起来很舒服
['这个', '手机', '屏幕', '很大', ',', '用', '起来', '很', '舒服']
p0: -28.0011339843 p1: -28.3622382739
classified as good 

input comment:再也不会买了,一点也不好,速度慢
['再也不会', '买', '了', ',', '一点', '也', '不好', ',', '速度慢']
p0: -12.2181082359 p1: -13.7462337788
classified as good 

input comment:破手机
['破', '手机']
p0: -10.3238494833 p1: -9.86890081319
classified as bad 

input comment:大屏幕手机,速度很快
['大屏幕', '手机', ',', '速度', '很快']
p0: -13.9816968282 p1: -16.6954460368
classified as good 

input comment:买来质量有问题,售后不解决,这下麻烦了
['买来', '质量', '有', '问题', ',', '售后', '不', '解决', ',', '这下', '麻烦', '了']
p0: -30.6306218629 p1: -28.9683740775
classified as bad

 

看这个结果还是挺有趣的,在只有非常有限的训练文本情况下,还不错。7条手工测试中,有2条判断错误,其他5条正确。

为了检查之后优化是否有效,首先我们不能用手工测试的方法,这样太麻烦了,接下来准备一个测试文本,并且标注好倾向性,然后程序运行后分别计算出 p0 p1 的正面负面与实际正面负面四个百分比,来衡量算法优化效果。

对于文本分类这里,可以改进的地方还有:

  • 加入停用词功能,去除这些常用词带来的干扰
  • 去除所有标点符号和空格等
  • 增加训练文本内容

使用 python 基于朴素贝叶斯进行文本分类学习笔记之一:开始
使用 python 基于朴素贝叶斯进行文本分类学习笔记之二:将原书程序修改并转换为类
使用 python 基于朴素贝叶斯进行文本分类学习笔记之三:中文内容的倾向性判断初步
使用 python 基于朴素贝叶斯进行文本分类学习笔记之四:增加测试文本和计算正确率
使用 python 基于朴素贝叶斯进行文本分类学习笔记之五:增加停用词

使用 python 基于朴素贝叶斯进行文本分类学习笔记之二:将原书程序修改并转换为类

朴素贝叶斯方法在文本分类中应用广泛,什么是朴素贝叶斯方法呢,wiki 上的定义如下:

在机器学习中,朴素贝叶斯分类器是一系列以假设特征之间强(朴素)独立下运用贝叶斯定理为基础的简单概率分类器。

所有朴素贝叶斯分类器都假定样本每个特征与其他特征都不相关,这是朴素(Naive)说法的由来,也是该算法的成立之本。

数学学得不太好,对于公式的理解有限,有些概念只能囫囵吞枣了,我更加关心的是应用场景和应用方法。

其实简单的想想也会知道,在文本分类中,我们假设词和词之间是不相关的,其实不太正确。

尽管是带着这些朴素思想和过于简单化的假设,但朴素贝叶斯分类器在很多复杂的现实情形中仍能够取得相当好的效果。

想看朴素贝叶斯整套公式推导过程的,可以移步这里,或者机器学习实战这本书上也有,此处就不再赘述了。

网上中文的介绍朴素贝叶斯用于文本分类的文章还是有一些,有兴趣的可以google 一下。

我们的目标是通过书中介绍的程序,来完成文本的倾向性分类,目前先按照两类进行分类,我简单的称之为正面和和负面的。

大致过程就是先进行正面和负面材料训练,计算得到 p0,p1,然后对材料进行测试,获得分类结果。

书中的源代码可以在这里下载,没有书也可以下载。代码属于第四章,不过书中举例主要还是在命令行环境下进行的,不是一个完整的可运行代码。

我做了如下修改:

  • 修改为 class 方式,方便程序调用
  • 修改为支持 python 3
  • 函数名称、变量名称和写法修改为支持 pep-8 标准

逐一解释一下代码:

下面是 class 类的一些变量,包括默认的训练文本和设定的倾向性。

# 训练 list, 默认内容, 原书中的内容
    train_doc_list = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                      ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                      ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
                      ['stop', 'posting', 'stupid', 'worthless', 'garbage'],
                      ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
                      ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
    # 倾向性向量, 0:正面 1:负面
    train_doc_sent_vec = [0, 1, 0, 1, 0, 1]

    # 单词列表集合
    word_list = []

    # 正面和负面概率, 先验概率
    p0_v = 0
    p1_v = 0
    p_ab = 0

 

下面是创建单词的集合,也就是将训练文本中所有的词合并到一起,不管出现几次都只保留一次。

    @staticmethod
    def create_word_list(data_list):
        # create empty set
        word_set = set([])
        for document in data_list:
            # union of the two sets
            word_set = word_set | set(document)
        return list(word_set)

 

下面这个函数是将输入的单词列表(需要训练或者测试的)根据上面的单词集合生成向量。

 @staticmethod
    def words_to_vec(word_list, new_word_list):
        vec = [0] * len(word_list)
        for word in new_word_list:
            if word in word_list:
                vec[word_list.index(word)] += 1
            else:
                pass
                # print("the word: %s is not in my Vocabulary!" % word)
        return vec

 

下面这个是朴素贝叶斯的算法,根据训练矩阵数据和预先设定的倾向性,来得到 p0,p1 概率。

@staticmethod
    def train_nb0(train_matrix, train_category):

        num_train_docs = len(train_matrix)
        num_words = len(train_matrix[0])
        p_ab = sum(train_category) / float(num_train_docs)

        # 创建给定长度的填满1的数组
        p0_num = ones(num_words)
        p1_num = ones(num_words)

        p0_d = 2.0
        p1_d = 2.0
        for i in range(num_train_docs):
            if train_category[i] == 1:
                p1_num += train_matrix[i]
                p1_d += sum(train_matrix[i])
            else:
                p0_num += train_matrix[i]
                p0_d += sum(train_matrix[i])

        p1_v = log(p1_num / p1_d)
        p0_v = log(p0_num / p0_d)
        return p0_v, p1_v, p_ab

 

然后是分类函数,根据上面计算得到的 p0,p1 概率,以及先验概率(如果正面和负面训练文本数量一样,就是 0.5),将输入的文本向量得到分类结果

    @staticmethod
    def classify_nb(vec, p0_vec, p1_vec, p_class1):
        # element-wise mult
        p0 = sum(vec * p0_vec) + log(1.0 - p_class1)
        p1 = sum(vec * p1_vec) + log(p_class1)
        print('p0:', p0, 'p1:', p1)
        if p1 > p0:
            return 1
        else:
            return 0

 

将上面的函数组合起来,得到下面这个完整的训练函数,上面大部分都是静态函数,外部不需要访问。

def train(self):
        # 生成单词列表集合
        self.word_list = self.create_word_list(self.train_doc_list)

        # 训练矩阵初始化
        train_matrix = []

        # 根据训练文档进行循环
        for post_in_doc in self.train_doc_list:
            # 构建训练矩阵, 将单词列表转化为向量
            train_matrix.append(self.words_to_vec(self.word_list, post_in_doc))

        # 根据训练矩阵和情感分析向量进行训练,得到
        self.p0_v, self.p1_v, self.p_ab = self.train_nb0(array(train_matrix), array(self.train_doc_sent_vec))

 

训练好了,就是测试函数,这是用来外部调用真正传入需要分类的文档或者校验正确率时候使用的。

 def testing_nb(self, test_word_list):

        # 对输入的内容转化为向量
        this_post_vec = array(self.words_to_vec(self.word_list, test_word_list))

        # 返回分类的值
        return self.classify_nb(this_post_vec, self.p0_v, self.p1_v, self.p_ab)

 

这个 navie bayes 代码会合并到我之前写的 fish_base 包中,代码也会放在 github 上,目前还在整理中。

刚才的 class 代码加上一个头,别忘记

from numpy import *


class ClassNaiveBayes:

 

其中的矩阵相关运算是通过 numpy 进行的,这个 python 下最好的数学计算库,性能不用怀疑。

调用上面 bayes 代码的例子代码,我将书中分散的测试代码合并了,并且通过类的方式调用:

import bayes

nb = bayes.ClassNaiveBayes()

nb.train()

test_list = ['love', 'my', 'dalmation']
print(test_list, 'classified as: ', nb.testing_nb(test_list))

test_list = ['stupid', 'garbage']
print(test_list, 'classified as: ', nb.testing_nb(test_list))

 

应该会得到书中一样的结果:

p0: -7.69484807238 p1: -9.82671449373
['love', 'my', 'dalmation'] classified as:  0
p0: -7.2093402566 p1: -4.70275051433
['stupid', 'garbage'] classified as:  1

 

接下来我尝试修改为针对中文内容进行分类,测试样本考虑用对于一款手机的评价,通过训练来区分是正面和还是负面的评论。

使用 python 基于朴素贝叶斯进行文本分类学习笔记之一:开始
使用 python 基于朴素贝叶斯进行文本分类学习笔记之二:将原书程序修改并转换为类
使用 python 基于朴素贝叶斯进行文本分类学习笔记之三:中文内容的倾向性判断初步
使用 python 基于朴素贝叶斯进行文本分类学习笔记之四:增加测试文本和计算正确率
使用 python 基于朴素贝叶斯进行文本分类学习笔记之五:增加停用词