词典分词

切分算法

完全切分

遍历文本找出在词典中的词语

正向,逆向,双向最长匹配

正向最长匹配 ,即在完全切分基础上加了一个规则:优先选取长度更长的单词
逆向最长匹配,即将正向匹配的扫描顺序改为从后到前
正向和逆向都有各自出现歧义的情况,融合两种匹配
双向最长匹配,即同时运行两者,若词数不同则返回词数少的,不然返回单字少的,否则返回逆向匹配

缺点

没有技术含量,消歧效果也不好,时间复杂度较高
针对时间复杂度,引入字典树,AC自动机
而消歧效果,则需要一些机器学习,深度学习的算法

字典树

基本原理

将文本分成单字,构建树
因为是树的结构,最坏情况的时间复杂度为O(logn),并且不需要存储所有切分结果,也比较省内存

python 版本代码实现

# -*- coding:utf-8 -*-



class TrieNode(object):
    def __init__(self, value=None, count=0, parent=None):
        # 值
        self.value = value
        # 频数统计
        self.count = count
        # 父结点
        self.parent = parent
        # 子节点,{value:TrieNode}
        self.children = {}


class Trie(object):
    def __init__(self):
        # 创建空的根节点
        self.root = TrieNode()

    def insert(self, sequence):
        """
        基操,插入一个序列
        :param sequence: 列表
        :return:
        """
        cur_node = self.root
        for item in sequence:
            if item not in cur_node.children:
                # 插入结点
                child = TrieNode(value=item, count=1, parent=cur_node)
                cur_node.children[item] = child
                cur_node = child
            else:
                # 更新结点
                cur_node = cur_node.children[item]
                cur_node.count += 1

    def search(self, sequence):
        """
        基操,查询是否存在完整序列
        :param sequence: 列表
        :return:
        """
        cur_node = self.root
        mark = True
        for item in sequence:
            if item not in cur_node.children:
                mark = False
                break
            else:
                cur_node = cur_node.children[item]
        # 如果还有子结点,说明序列并非完整
        if cur_node.children:
            mark = False
        return mark

    def delete(self, sequence):
        """
        基操,删除序列,准确来说是减少计数
        :param sequence: 列表
        :return:
        """
        mark = False
        if self.search(sequence):
            mark = True
            cur_node = self.root
            for item in sequence:
                cur_node.children[item].count -= 1
                if cur_node.children[item].count == 0:
                    cur_node.children.pop(item)
                    break
                else:
                    cur_node = cur_node.children[item]
        return mark

    def search_part(self, sequence, prefix, suffix, start_node=None):
        """
        递归查找子序列,返回前缀和后缀结点
        此处简化操作,仅返回一位前后缀的内容与频数
        :param sequence: 列表
        :param prefix: 前缀字典,初始传入空字典
        :param suffix: 后缀字典,初始传入空字典
        :param start_node: 起始结点,用于子树的查询
        :return:
        """
        #递归时设置父节点
        if start_node:
            cur_node = start_node
            prefix_node = start_node.parent
        else:
            cur_node = self.root
            prefix_node = self.root
        # mark代表查找是否成功。某一次查找失败,则将mark置false,不需要提取其前缀和后缀
        mark = True
        # 必须从第一个结点开始对比
        for i in range(len(sequence)):
            if i == 0:
                if sequence[i] != cur_node.value:
                    for child_node in cur_node.children.values():
                        self.search_part(sequence, prefix, suffix, child_node)
                    mark = False
                    #如果当前节点不是需要查找的句子的第一个词,则停止继续查找
                    break
            else:
                if sequence[i] not in cur_node.children:
                    for child_node in cur_node.children.values():
                        self.search_part(sequence, prefix, suffix, child_node)
                    mark = False
                    break
                else:
                    cur_node = cur_node.children[sequence[i]]
        if mark:
            if prefix_node.value:
                # 前缀数量取序列词中最后一词的频数
                if prefix_node.value in prefix:
                    prefix[prefix_node.value] += cur_node.count
                else:
                    prefix[prefix_node.value] = cur_node.count
            for suffix_node in cur_node.children.values():
                if suffix_node.value in suffix:
                    suffix[suffix_node.value] += suffix_node.count
                else:
                    suffix[suffix_node.value] = suffix_node.count
            # 即使找到一部分还需继续查找子结点
            for child_node in cur_node.children.values():
                self.search_part(sequence, prefix, suffix, child_node)


if __name__ == "__main__":
    trie = Trie()
    texts = ["葬爱少年葬爱少年睦洲立哈哈", "葬爱少年阿西吧", "烈烈风中", "忘记了爱",
             "埋葬了爱"]
    for text in texts:
        trie.insert(text)
    markx = trie.search("忘记了爱")
    print(markx)
    markx = trie.search("忘记了")
    print(markx)
    markx = trie.search("忘记爱")
    print(markx)
    markx = trie.delete("葬爱少年王周力")
    print(markx)
    prefixx = {}
    suffixx = {}
    trie.search_part("葬爱少年", prefixx, suffixx)
    print(prefixx)
    print(suffixx)

适用场景

词频统计,字符串检索,停用词过滤

缺点

只是简单的切分,并非分词,消歧效果,OOV识别率都非常不理想
时间复杂度尚可改进.此处引入AC自动机

AC自动机

基本原理

和KMP算法有些类似
同样需要建树,在查找树的同时,如果遇到匹配失败,则通过fail指阻止回溯,将时间复杂度降至o(n)

python代码

# -*- coding:utf-8 -*-
from collections import defaultdict
class TrieNode(object):
    def __init__(self, value=None):
        # 值
        self.value = value
        # fail指针
        self.fail = None
        # 尾标志:标志为i表示第i个模式串串尾,默认为0
        self.tail = 0
        # 子节点,{value:TrieNode}
        self.children = {}


class Trie(object):
    def __init__(self, words):
        print("初始化")
        # 根节点
        self.root = TrieNode()
        # 模式串个数
        self.count = 0
        self.words = words
        for word in words:
            self.insert(word)
        self.ac_automation()
        print("初始化完毕")

    def insert(self, sequence):
        """
        基操,插入一个字符串
        :param sequence: 字符串
        :return:
        """
        self.count += 1
        cur_node = self.root
        for item in sequence:
            if item not in cur_node.children:
                # 插入结点
                child = TrieNode(value=item)
                cur_node.children[item] = child
                cur_node = child
            else:
                cur_node = cur_node.children[item]
        cur_node.tail = self.count

    def ac_automation(self):
        """
        构建失败路径
        :return:
        """
        queue = [self.root]
        # BFS遍历字典树
        while len(queue):
            temp_node = queue[0]
            # 取出队首元素
            queue.remove(temp_node)
            for value in temp_node.children.values():
                # 根的子结点fail指向根自己
                if temp_node == self.root:
                    value.fail = self.root
                else:
                    # 转到fail指针
                    p = temp_node.fail

                    while p:#while循环的意义是:当p不是none且没有合适的匹配,无法构建fail指针时,将fail指针还原到根节点
                        # 若当前节点的值有在当前节点的父节点fail指针指向的节点的子节点的值时,构建fail指针,终止循环
                        if value.value in p.children:
                            value.fail = p.children[value.value]
                            break
                        # 转到fail指针继续回溯
                        p = p.fail
                    # 若为None,表示当前结点值在之前都没出现过,则其fail指向根结点
                    if not p:
                        value.fail = self.root
                # 将当前结点的所有子结点加到队列中
                queue.append(value)

    def search(self, text):
        """
        模式匹配
        :param self:
        :param text: 长文本
        :return:
        """
        p = self.root
        # 记录匹配起始位置下标
        start_index = 0
        # 成功匹配结果集
        rst = defaultdict(list)
        for i in range(len(text)):
            single_char = text[i]
            while single_char not in p.children and p is not self.root:
                p = p.fail
            # 有一点瑕疵,原因在于匹配子串的时候,若字符串中部分字符由两个匹配词组成,此时后一个词的前缀下标不会更新
            # 这是由于KMP算法本身导致的,目前与下文循环寻找所有匹配词存在冲突
            # 但是问题不大,因为其标记的位置均为匹配成功的字符
            if single_char in p.children and p is self.root:
                start_index = i
            # 若找到匹配成功的字符结点,则指向那个结点,否则指向根结点
            if single_char in p.children:
                p = p.children[single_char]
            else:
                start_index = i
                p = self.root
            temp = p
            while temp is not self.root:
                # 尾标志为0不处理,但是tail需要-1从而与敏感词字典下标一致
                # 循环原因在于,有些词本身只是另一个词的后缀,也需要辨识出来
                if temp.tail:
                    rst[self.words[temp.tail - 1]].append((start_index, i))
                temp = temp.fail
        return rst

if __name__ == "__main__":
    test_words = ["he", "she", "hers", "his"]
    test_text = """ahishers"""
    model = Trie(test_words)
    # defaultdict(<class 'list'>, {'his': [(1, 3)], 'she': [(1, 5)], 'he': [(1, 5)], 'hers': [(1, 7)]})
    print(str(model.search(test_text)))

优点

时间复杂度低

缺点:

与字典树相似.另外,大量使用指针等,用空间换时间

适用场景:

与字典树相似

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值