自然语言处理N天-Day0802从自然语言处理角度看HMM和CRF

新建 Microsoft PowerPoint 演示文稿 (2).jpg

说明:本文依据《中文自然语言处理入门实战》完成。目前网上有不少转载的课程,我是从GitChat上购买。

近几年在自然语言处理领域中,HMM(隐马尔可夫模型)和 CRF(条件随机场)算法常常被用于分词、句法分析、命名实体识别、词性标注等。
由于两者之间有很大的共同点,所以在很多应用上往往是重叠的,但在命名实体、句法分析等领域 CRF 似乎更胜一筹。通常来说如果做自然语言处理,这两个模型应该都要了解,下面我们来看看本文的内容。

第八课 从自然语言处理角度看HMM和CRF

基于HMM训练中文分词器

1.模型介绍

HMM模型是由一个“五元组”构成的集合。
1.StatusSet:状态值集合,状态值集合为 (B, M, E, S),其中 B 为词的首个字,M 为词中间的字,E 为词语中最后一个字,S 为单个字,B、M、E、S 每个状态代表的是该字在词语中的位置。
2.ObservedSet:观察值集合,观察值集合就是所有语料的汉字,甚至包括标点符号所组成的集合。
3.TransProbMatrix:转移概率矩阵,状态转移概率矩阵的含义就是从状态 X 转移到状态 Y 的概率,是一个4×4的矩阵,即 {B,E,M,S}×{B,E,M,S}。
4.EmitProbMatrix:发射概率矩阵,发射概率矩阵的每个元素都是一个条件概率,代表 P(Observed[i]|Status[j]) 概率。
5.InitStatus:初始状态分布,初始状态概率分布表示句子的第一个字属于 {B,E,M,S} 这四种状态的概率。
使用HMM进行分词,要解决的问题就是参数已知的情况下,如何求解状态值序列。

2.语料准备

本次训练使用的预料 syj_trainCorpus_utf8.txt 是我爬取的短文本处理生成的。整个语料大小 264M,包含1116903条数据,UTF-8 编码,词与词之间用空格隔开,用来训练分词模型。
教程给的是CSDN地址,很没劲又设置了1分的付费下载,搞得必须去充值才行,于是就在淘宝付费代下,以下为百度云盘地址

链接:https://pan.baidu.com/s/1eyTPOD7kuDdcIVDLVl6OlA
提取码:qf5k

3.实现

import pickle
import json

STATES = {'B', 'M', 'E', 'S'}
EPS = 0.0001
seg_stop_words = {" ", ",", "。", "“", "”", '“', "?", "!", ":", "《", "》", "、", ";", "·", "‘ ", "’", "──", ",", ".", "?",
                  "!", "`", "~", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "[", "]", "{", "}",
                  '"', "'", "<", ">", "\\", "|" "\r", "\n", "\t"}

class HMM_Model:
    def __init__(self):
        self.trans_mat = {}  # trans_mat:状态转移矩阵,trans_mat[state1][state2] 表示训练集中由 state1 转移到 state2 的次数。
        self.emit_mat = {}  # emit_mat:观测矩阵,emit_mat[state][char] 表示训练集中单字 char 被标注为 state 的次数。
        self.init_vec = {}  # init_vec:初始状态分布向量,init_vec[state] 表示状态 state 在训练集中出现的次数。
        self.state_count = {}  # state_count:状态统计向量,state_count[state]表示状态 state 出现的次数。
        self.states = {}
        self.inited = False

    # 初始化第一个方法中的数据结构
    def setup(self):
        for state in self.states:
            # build trans_mat
            self.trans_mat[state] = {}
            for target in self.states:
                self.trans_mat[state][target] = 0.0
            self.emit_mat[state] = {}
            self.init_vec[state] = 0
            self.state_count[state] = 0
        self.inited = True

    # filename 指定模型名称,默认模型名称为 hmm.json,这里提供两种格式的保存类型,JSON 或者 pickle 格式,通过参数 code 来决定,code 的值为 code='json' 或者 code = 'pickle',默认为 code='json'
    def save(self, filename="hmm.json", code='json'):
        fw = open(filename, 'w', encoding='utf-8')
        data = {
            "trans_mat": self.trans_mat,
            "emit_mat": self.emit_mat,
            "init_vec": self.init_vec,
            "state_count": self.state_count
        }
        if code == "json":
            txt = json.dumps(data)
            txt = txt.encode('utf-8').decode('unicode-escape')
            fw.write(txt)
        elif code == "pickle":
            pickle.dump(data, fw)
        fw.close()

    # filename 指定模型名称,默认模型名称为 hmm.json,这里提供两种格式的保存类型,JSON 或者 pickle 格式,通过参数 code 来决定,code 的值为 code='json' 或者 code = 'pickle',默认为 code='json'
    def load(self, filename="hmm.json", code="json"):
        fr = open(filename, 'r', encoding='utf-8')
        if code == "json":
            txt = fr.read()
            model = json.loads(txt)
        elif code == "pickle":
            model = pickle.load(fr)
        self.trans_mat = model["trans_mat"]
        self.emit_mat = model["emit_mat"]
        self.init_vec = model["init_vec"]
        self.state_count = model["state_count"]
        self.inited = True
        fr.close()

    # 用来训练模型,因为使用的标注数据集, 因此可以使用更简单的监督学习算法,训练函数输入观测序列和状态序列进行训练, 依次更新各矩阵数据。类中维护的模型参数均为频数而非频率, 这样的设计使得模型可以进行在线训练,使得模型随时都可以接受新的训练数据继续训练,不会丢失前次训练的结果
    def do_train(self, observes, states):
        if not self.inited:
            self.setup()
        for i in range(len(states)):
            if i == 0:
                self.init_vec[states[0]] += 1
                self.state_count[states[0]] += 1
            else:
                self.trans_mat[states[i - 1]][states[i]] += 1
                self.state_count[states[i]] += 1
                if observes[i] not in self.emit_mat[states[i]]:
                    self.emit_mat[states[i]][observes[i]] = 1
                else:
                    self.emit_mat[states[i]][observes[i]] += 1

    # 在进行预测前,需将数据结构的频数转换为频率
    def get_prob(self):
        init_vec = {}
        trans_mat = {}
        emit_mat = {}
        default = max(self.state_count.values())
        for key in self.init_vec:
            if self.state_count[key] != 0:
                init_vec[key] = float(self.init_vec[key]) / self.state_count[key]
            else:
                init_vec[key] = float(self.init_vec[key]) / default

        for key1 in self.trans_mat:
            trans_mat[key1] = {}
            for key2 in self.trans_mat[key1]:
                if self.state_count[key1] != 0:
                    trans_mat[key1][key2] = float(self.trans_mat[key1][key2]) / self.state_count[key1]
                else:
                    trans_mat[key1][key2] = float(self.trans_mat[key1][key2]) / default

        for key1 in self.emit_mat:
            emit_mat[key1] = {}
            for key2 in self.emit_mat[key1]:
                if self.state_count[key1] != 0:
                    emit_mat[key1][key2] = float(self.emit_mat[key1][key2]) / self.state_count[key1]
                else:
                    emit_mat[key1][key2] = float(self.emit_mat[key1][key2]) / default
        return init_vec, trans_mat, emit_mat

    # 采用Viterbi(维特比)算法求得最优路径
    def do_predict(self, sequence):
        tab = [{}]
        path = {}
        init_vec, trans_mat, emit_mat = self.get_prob()

        # 初始化
        for state in self.states:
            tab[0][state] = init_vec[state] * emit_mat[state].get(sequence[0], EPS)
            path[state] = [state]

        # 创建动态搜索表
        for t in range(1, len(sequence)):
            tab.append({})
            new_path = {}
            for state1 in self.states:
                items = []
                for state2 in self.states:
                    if tab[t - 1][state2] == 0:
                        continue
                    prob = tab[t - 1][state2] * trans_mat[state2].get(state1, EPS) * emit_mat[state1].get(sequence[t],
                                                                                                          EPS)
                    items.append((prob, state2))
                best = max(items)
                tab[t][state1] = best[0]
                new_path[state1] = path[best[1]] + [state1]
            path = new_path

        # 搜索最有路径
        prob, state = max([(tab[len(sequence) - 1][state], state) for state in self.states])
        return path[state]


def get_tags(src):
    tags = []
    if len(src) == 1:
        tags = ['S']
    elif len(src) == 2:
        tags = ['B', 'E']
    else:
        m_num = len(src) - 2
        tags.append('B')
        tags.extend(['M'] * m_num)
        tags.append('E')
    return tags


def cut_sent(src, tags):
    word_list = []
    start = -1
    started = False

    if len(tags) != len(src):
        return None

    if tags[-1] not in {'S', 'E'}:
        if tags[-2] in {'S', 'E'}:
            tags[-1] = 'S'
        else:
            tags[-1] = 'E'

    for i in range(len(tags)):
        if tags[i] == 'S':
            if started:
                started = False
                word_list.append(src[start:i])
            word_list.append(src[i])
        elif tags[i] == 'B':
            if started:
                word_list.append(src[start:i])
            start = i
            started = True
        elif tags[i] == 'E':
            started = False
            word = src[start:i + 1]
            word_list.append(word)
        elif tags[i] == 'M':
            continue
    return word_list


class HMMSoyoger(HMM_Model):
    def __init__(self, *args, **kwargs):
        super(HMMSoyoger, self).__init__(*args, **kwargs)
        self.states = STATES
        self.data = None

    # 加载训练数据
    def read_txt(self, filename):
        self.data = open(filename, 'r', encoding="utf-8")

    # 模型训练函数
    def train(self):
        if not self.inited:
            self.setup()

        for line in self.data:
            line = line.strip()
            if not line:
                continue

                # 观测序列
            observes = []
            for i in range(len(line)):
                if line[i] == " ":
                    continue
                observes.append(line[i])

            # 状态序列
            words = line.split(" ")

            states = []
            for word in words:
                if word in seg_stop_words:
                    continue
                states.extend(get_tags(word))
            # 开始训练
            if (len(observes) >= len(states)):
                self.do_train(observes, states)
            else:
                pass

    # 模型分词预测
    def lcut(self, sentence):
        try:
            tags = self.do_predict(sentence)
            return cut_sent(sentence, tags)
        except:
            return sentence

#训练模型,首先实例化 HMMSoyoger 类,然后通过 read_txt() 方法加载语料,再通过 train() 进行在线训练,如果训练语料比较大,可能需要等待一点时间
soyoger=HMMSoyoger()
soyoger.read_txt(r'C:\Users\01\Desktop\机器学习作业\sklearn+tensorflow\corpus_data\syj_trainCorpus_utf8.txt')
soyoger.train()

print(soyoger.lcut("中国的人工智能发展进入高潮阶段。"))
print(soyoger.lcut("中文自然语言处理是人工智能技术的一个重要分支。"))
print(soyoger.lcut("关注可了解更多的教程及排版技巧。问题或建议,请公众号留言"))
print(soyoger.lcut("南京市长是个好同志"))
print(soyoger.lcut("南京市长江大桥"))

我按照教程敲下来的代码,分词结果明显有问题……两个两个分词,就这教程还说好……
['中国', '的', '人工', '智能', '发展', '进入', '高潮', '阶段', '。']
['中文', '自然', '语言', '处理', '是', '人工', '智能', '技术', '的一', '个重', '要分', '支。']
['关注', '可了', '解更', '多的', '教程', '及排', '版技', '巧。', '问题', '或建', '议,', '请公', '众号', '留言']
['南京', '市长', '是', '个好', '同志']
['南京', '市长', '江大', '桥']

试一下HanLP分词

from pyhanlp import *

print(HanLP.segment(r'你好,欢迎在Python中调用HanLP的API'))
testCases = [
    "中国的人工智能发展进入高潮阶段。",
    "中文自然语言处理是人工智能技术的一个重要分支。",
    "关注可了解更多的教程及排版技巧。问题或建议,请公众号留言",
    "南京市长是个好同志",
    "南京市长江大桥"]
for sentence in testCases: print(HanLP.segment(sentence))

#[你好/vl, ,/w, 欢迎/v, 在/p, Python/nx, 中/f, 调用/v, HanLP/nx, 的/ude1, API/nx]
# [中国/ns, 的/ude1, 人工智能/n, 发展/vn, 进入/v, 高潮/n, 阶段/n, 。/w]
# [中文/nz, 自然语言处理/nz, 是/vshi, 人工智能/n, 技术/n, 的/ude1, 一个/mq, 重要/a, 分支/n, 。/w]
# [关注/v, 可/v, 了解/v, 更多/ad, 的/ude1, 教程/n, 及/cc, 排版/vn, 技巧/n, 。/w, 问题/n, 或/c, 建议/n, ,/w, 请/v, 公众/n, 号/q, 留言/n]
# [南京/ns, 市长/nnt, 是/vshi, 个/q, 好/a, 同志/n]
# [南京市/ns, 长江大桥/nz]

结果一目了然,干嘛还要重复造轮子……
关于CRF分词,同样HanLP也有,不过需要通过调用JClass来完成对Java的底层API的调用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容