自然语言处理第一次实验报告

 

自然语言处理第一次实验报告班级:计科2102 姓名:高星杰 学号:2021307220712正向、逆向分词在做正向和逆向分词实验之前我们必须要了解什么是正向分词,什么是逆向分词,以及一些常用的分词算法的原理。正向分词和逆向分词是中文分词中常见的两种基本方法,它们分别从文本的开头和结尾开始,按照字典或规则逐个匹配,确定文本中的词语边界。以下是正向和逆向分词常见的算法:

自然语言处理第一次实验报告

班级:计科2102 姓名:高星杰 学号:2021307220712

正向、逆向分词

在做正向和逆向分词实验之前我们必须要了解什么是正向分词,什么是逆向分词,以及一些常用的分词算法的原理。

正向分词和逆向分词是中文分词中常见的两种基本方法,它们分别从文本的开头和结尾开始,按照字典或规则逐个匹配,确定文本中的词语边界。以下是正向和逆向分词常见的算法:

正向分词算法:

  1. 最大正向匹配法(MM)
    • 从文本的开头开始,选择字典中最长的词作为分词的候选词,然后将文本指针向后移动,重复这个过程直至分词完成或者文本结束。
  2. 正向最大概率分词(Forward Maximum Probability Segmentation)
    • 结合概率模型和最大正向匹配,利用统计语言模型或者机器学习模型计算每个词在文本中出现的概率,然后选择概率最高的词作为分词结果。
  3. 正向最长路径分词(Forward Longest Path Segmentation)
    • 基于图论的思想,将文本中的每个字看作图中的节点,根据字典中词的组合构建一个有向图,通过寻找最长路径来确定最可能的分词结果。
  4. DAG(有向无环图)分词
    • 将文本构建成一个有向无环图,图中的节点为文本中的每个字符或者词的起始位置,边代表可能的词组合,通过动态规划或者其他算法求解最佳路径,得到分词结果。

逆向分词算法:

  1. 最大逆向匹配法(RMM)
    • 从文本的结尾开始,选择字典中最长的词作为分词的候选词,然后将文本指针向前移动,重复这个过程直至分词完成或者文本开头。
  2. 逆向最大概率分词(Reverse Maximum Probability Segmentation)
    • 类似于正向最大概率分词,但是是从文本结尾开始向开头匹配,计算每个词在文本中出现的概率,选择概率最高的词作为分词结果。
  3. 逆向最长路径分词(Reverse Longest Path Segmentation)
    • 与正向最长路径分词类似,但是是从文本结尾开始构建有向图,寻找最长路径确定最可能的分词结果。

这些算法在实际中根据具体需求和文本特点选择,最大匹配法和最大概率分词是最常用的基础算法,而其他方法则可能在特定场景下有更好的表现。

大概了解了这两种分词算法后我们可以各选择一种进行实现,本次实验选取的是:正向最大匹配逆向最大匹配

下面将具体的对这两种算法的实现过程进行讲解。

在进行算法实现之前我们要先明确我们有什么: ’ 96年人民日报语料 ‘、’词典’

我们要使用 ‘词典’ 对 ‘96年人民日报语料’ 进行划分,并且无论是正向还是逆向匹配算法我们都是使用贪心思想来处理的。

正向最大匹配分词

所谓正向最大匹配算法就是以正向对词或句子进行匹配,并且匹配要从最大长度的词开始到最小长度的词,匹配到则划分为一个词。

下面是对代码的逐行解释:

def fwd_mm_seg(wordDict, maxLen, str):
    """正向最大匹配分词
    @param wordDict:    词表
    @param maxLen:      词最大长度(自定义)
    @param str:         待分词的字串
    @return:            分词序列(List)
    """
    wordList = []  # 定义的分词后的结果,也就是最终的分词序列
    segStr = str  # 定义需要分词的字串
    segStrLen = len(segStr)  # 获得需要分词的字串的长度
    while segStrLen > 0:  # 正向遍历需要分词的字串
        if segStrLen > maxLen:  # 如果需要分词的字串长度大于最大词长,则按最大词长分词
            wordLen = maxLen  # 词长为最大词长
        else:  # 如果需要分词的字串长度小于最大词长,则按字串长度分词
            wordLen = segStrLen  # 词长为字串长度
        subStr = segStr[0:wordLen]  # 取字串的前wordLen个字符作为待匹配的词
        while wordLen > 1:  # 词长逐渐减小直到为1 也就是‘最大匹配’的代码体现
            if subStr in wordDict:  # 如果待匹配的词在词表中
                break  # 则跳出循环
            else:  # 如果待匹配的词不在词表中
                wordLen = wordLen - 1  # 则词长减1
                subStr = subStr[0:wordLen]  # 取字串的前wordLen个字符作为待匹配的词
        wordList.append(subStr)  # 将待匹配的词添加到分词序列中
        segStr = segStr[wordLen:]  # 将待匹配的词从字串中去除
        segStrLen = segStrLen - wordLen  # 将待匹配的词的长度从字串长度中去除
    return wordList  # 返回分词序列

逆向最大匹配分词

和上面正向最大匹配分词的区别只有匹配的方向变了是从末尾开始,然后反方向前进。

下面是逐行的代码分析:

def bwd_mm_seg(wordDict, maxLen, str):
    """逆向最大匹配分词
    @param wordDict:    词表
    @param maxLen:      词最大长度(自定义)
    @param str:         待分词的字串
    @return:            分词序列(List)
    """
    wordList = []  # 定义的分词后的结果,也就是最终的分词序列
    segStr = str  # 定义需要分词的字串
    segStrLen = len(segStr)  # 获得需要分词的字串的长度
    while segStrLen > 0:  # 逆向遍历需要分词的字串
        if segStrLen > maxLen:  # 如果需要分词的字串长度大于最大词长,则按最大词长分词
            wordLen = maxLen  # 词长为最大词长
        else:  # 如果需要分词的字串长度小于最大词长,则按字串长度分词
            wordLen = segStrLen  # 词长为字串长度
        subStr = segStr[-wordLen:None]  # 取字串的后wordLen个字符作为待匹配的词
        while wordLen > 1:  # 如果词长大于1
            if subStr in wordDict:  # 如果待匹配的词在词表中
                break  # 则跳出循环
            else:  # 如果待匹配的词不在词表中
                wordLen = wordLen - 1  # 则词长减1
                subStr = subStr[-wordLen:None]  # 取字串的后wordLen个字符作为待匹配的词
        wordList.append(subStr)  # 将待匹配的词添加到分词序列中
        segStr = segStr[0: -wordLen]  # 将待匹配的词从字串中去除
        segStrLen = segStrLen - wordLen  # 将待匹配的词的长度从字串长度中去除
    wordList.reverse()  # 将分词序列逆序
    return wordList  # 返回分词序列

出现的问题以及解决方案

我们仅仅使用上面的代码是不够,为什么呢?因为还有一个问题没有考虑到,我们要以什么单位来进行分词?显然直接以全文为单位进行分词是效率不够的,那样的算法的复杂度比较高,所以我们要寻找一个可行的单位。

这里我们使用的以一个自然段为单位进行分词。

def file_seg_process(filename, method):
    '''
    @param filename: 文件名
    @param method:   分词算法 { 0:正向,1:逆向 }
    '''
    # 打开文件
    fp_dict = open('dict.txt', encoding=CODEC)  # 打开词典
    fp_input = open('corpus/' + filename, encoding=CODEC)  # 打开语料
    fp_output = open('corpus_seg/' + filename, 'w', encoding=CODEC)  # 打开输出文件

    wordDict = {}  # 定义词典
    # 读取字典到内存中
    for eachWord in fp_dict:  # 对词典中的每一行操作
        wordDict[u(eachWord.split()[0].strip(), CODEC)] = 1  # 将词典中的词存入字典中

    # 对input每一行操作
    str = ''  # 定义一个空字符串用来存储每次分词的一个单位字串
    for eachLine in fp_input:  # 对语料中的每一行操作
        line_out = ''  # 定义输出行
        # 每一段作为一行输入给分词函数
        sub = eachLine.rstrip()  # 去除每一行的右边的空格
        if not sub.startswith('  ') and not sub.startswith('  '):  # 如果每一行不是以两个空格或者全角空格开头 则加入的到单位字串 注意这里是修改过后的代码添加的中文空格
            str += sub  # 将每一行加入到str中
            continue  # 跳过本次循环

        strlen = len(str)  # 计算str的长度
        while strlen > 0:  # 当str的长度大于0时
            # 英文字符或数字--原文输出
            m = re.match('\w+', str)  # 匹配英文字符或数字
            if m is not None:  # 如果匹配成功
                subStr = m.group()  # 将匹配到的字符赋值给subStr
                line_out += subStr + '/'  # 将匹配到的字符加入到输出行中
                subLen = len(subStr)  # 计算匹配到的字符的长度
                str = str[subLen:]  # 将匹配到的字符从str中去除
                strlen = strlen - subLen  # 将匹配到的字符的长度从str的长度中去除
                continue  # 跳过本次循环
            # 短句结尾标志--输出换行
            if str[0:1] in [',', '', '!', '?', ':']:  # 如果str的第一个字符是逗号、句号、感叹号、问号、冒号中的一个
                subStr = str[0:1]  # 将第一个字符赋值给subStr
                line_out += '\n'  # 将换行符加入到输出行中
                subLen = len(subStr)  # 计算第一个字符的长度
                str = str[subLen:]  # 将第一个字符从str中去除
                strlen = strlen - subLen  # 将第一个字符的长度从str的长度中去除
            # 汉字--分词处理,输出 词/词
            # str = str.encode("unicode_escape")
            # m = re.match('[\u4e00-\u9fa5]+', str)
            m = re.match('[^\x00-\xff]+', str)  # 匹配汉字
            if m is not None:  # 如果匹配成功
                subStr = m.group()  # 将匹配到的汉字赋值给subStr
                if method == 0:  # 正向最大匹配
                    # 正向最大匹配
                    wordList = fwd_mm_seg(wordDict, 8, subStr)  # 调用正向最大匹配函数
                else:
                    # 逆向最大匹配
                    wordList = bwd_mm_seg(wordDict, 8, subStr)  # 调用逆向最大匹配函数
                line_out += wordList[0] + '/'  # 将分词后的第一个词加入到输出行中
                for eachWord in wordList[1:]:  # 对分词后的每一个词操作
                    line_out += eachWord + '/'  # 将每一个词加入到输出行中
                subLen = len(subStr)  # 计算匹配到的汉字的长度
                str = str[subLen:]  # 将匹配到的汉字从str中去除
                strlen = strlen - subLen  # 将匹配到的汉字的长度从str的长度中去除
                continue  # 跳过本次循环
            # 其他特殊字符--跳过
            str = str[1:]  # 将第一个字符从str中去除
            strlen = strlen - 1  # 将第一个字符的长度从str的长度中去除
        # 跳过处理后为空行的段落
        if len(line_out.strip()) == 0:  # 如果输出行是空行
            continue  # 跳过本次循环
        # 写入文件
        fp_output.write(line_out + '\n')  # 将输出行写入到输出文件中
        str = sub  # 将每一行赋值给str
    # close file
    fp_input.close()  # 关闭输入文件
    fp_dict.close()  # 关闭词典
    fp_output.close()  # 关闭输出文件

在实现代码的过程中出现了有的文本不能正确匹配的问题,也就是分词的单位字串(一个段落)没有识别出来,整篇文章识别成了一个单位字串,而没有进行分词处理。

而导致没有识别出来的问题是,每段开头的空格符号没有识别出来,因为有的文章是用的中文空格,有的用的英文空格,所以我们这里判断的条件多加一个判断中文空格的条件就可以:

        if not sub.startswith('  ') and not sub.startswith('  '):  # 如果每一行不是以两个空格或者全角空格开头 则加入的到单位字串 注意这里是修改过后的代码添加的中文空格
            str += sub  # 将每一行加入到str中
            continue  # 跳过本次循环

一元、二元词频统计

同样的在实现一元、二元词频统计时,我们要先了解的其算法思想和原理。

一元词频统计和二元词频统计是自然语言处理中常用的文本分析方法。

一元词频统计算法

一元词频统计指统计文本中每个词的出现次数。通过一元词频统计可以找到文本中最常出现的词、关键词等。一元词频统计比较简单,主要包括文本分词、词频统计两个步骤。

二元词频统计算法

二元词频统计指统计文本中词与词之间共现的次数,即两个词作为邻近词组同时出现的次数。二元词频可以反映词与词之间的语义关联性。常用的二元词频统计方法有:

  1. 马尔可夫链模型:按照词出现的顺序建模,统计词与词之间的转移概率。
  2. 互信息法:用来衡量两个随机事件同时发生的概率。利用互信息可以判断两个词的关联程度。
  3. 数据挖掘方法:如Apriori算法,可以高效发现词频数据集中出现次数高的词组。
  4. 神经网络模型:如word2vec中的Skip-gram和CBOW模型,可以获得词向量之间的语义关联性。

统计一元词频和二元词频都是自然语言理解的基础工作,后续可以应用于文本分类、情感分析、词向量训练等任务。这两个统计方法有良好的扩展性和通用性。

本次实验我们使用的是普通的一元词频统计算法和基于统计的二元词频统计

普通的一元词频统计(Unigram Frequency)

一元词频统计的步骤主要包括文本分词、词频统计两个步骤,而步骤一我们刚才以及实现了,所以我们可以直接使用分词后的结果进行统计即可,具体来说,对于每一个词,它检查是否是中文字符(使用了正则表达式进行匹配)。如果是中文字符,则将其作为一元词(unigram)进行统计,并增加其出现次数。

基于统计的二元词频统计(Bigram Frequency)

这种方法是基于简单的统计逻辑来识别中文字符并计算相邻词语之间的频率,具体来说对于二元词频(bigram),它检查每个词是否是中文字符,并且检查其前一个词是否也是中文字符,然后将这两个词组合在一起作为一个二元词进行统计,并增加其出现次数。

下面是逐行代码分析:

class NGram(object):
    '''n元词频统计'''

    def __init__(self):  # 初始化
        self.unigram = {}  # 一元词频
        self.bigram = {}  # 二元词频
        self.wordDict = []  # 词表
        # dict = open('dict.txt')
        # for line in dict:
        #     if len(line.strip()) > 0:
        #         self.wordDict.append(line.strip())

    def scan(self, lines):
        '''
        逐行扫描,ngram结果记录到文件中
        @param    sentence    list{str}
        @return   none
        '''
        words = []  # 词列表
        for line in lines:  # 逐行扫描
            # 统计n元词频
            words.append('<li>')  # 用<li>标记每一行的开始
            wordlist = [  # 用正则表达式匹配出每一个词
                w
                for w in list(line.split('/'))  # 去掉用来分割词的/
                if len(w.strip()) > 0  # 去除每一行两边的空格
            ]
            words.extend(wordlist)  # 将每一行的词加入到词列表中
            words.append('</li>')  # 用</li>标记每一行的结束

        self.ngram(words)  # 统计ngram

        print('[ Hashed ]')  # 输出提示信息

        # unigram
        file = open("freq/word_freq.txt", "w", encoding=CODEC)  # 打开文件
        for key, value in self.unigram.items():  # 逐个词统计
            file.write("%s\t%d\n" % (key, value))  # 将词和词频写入到文件中
        file.close()  # 关闭文件
        print('[ Unigram file finish ]')  # 输出提示信息

        # bigram
        file = open("freq/bigram_freq.txt", "w", encoding=CODEC)  # 打开文件
        for key, value in self.bigram.items():  # 逐个词统计
            file.write("%s\t%d\n" % (key, value))  # 将词和词频写入到文件中
        file.close()  # 关闭文件
        print('[ Bigram file finish ]')  # 输出提示信息

    def ngram(self, words):
        '''
        统计ngram
        @param    words       list{str}
        @return   none
        '''
        partten = '([\u4e00-\u9fa5])+'  # 匹配中文
        # unigram
        for i in range(0, len(words)):  # 逐个词统计
            if not re.search(partten, words[i]):  # 查看当前词是不是中文
                continue  # 不是中文则跳过
            key = words[i]  # 当前词
            if key not in self.unigram:  # 如果当前词不在unigram中
                self.unigram[key] = 0  # 则将当前词加入到unigram中
            self.unigram[key] += 1  # 否则当前词的词频加1

        # bigram
        for i in range(1, len(words)):  # 逐个词统计
            if not re.search(partten, words[i]):  # 查看当前词是不是中文
                continue  # 不是中文则跳过
            if not re.search(partten, words[i - 1]):  # 查看上一个词是不是中文
                # 查看上一个词是不是同一行的中文词能实现换行的效果也能排除<li>的影响
                continue  # 不是中文则跳过

            key = words[i] + '|' + words[i - 1]  # 当前词和上一个词组成的词
            if key not in self.bigram:  # 如果当前词和上一个词组成的词不在bigram中
                self.bigram[key] = 0  # 则将当前词和上一个词组成的词加入到bigram中
            self.bigram[key] += 1  # 否则当前词和上一个词组成的词的词频加1

出现的问题以及解决方案

问题:

在进行词频统计时发现会把的<li></li>等用来表示换行的符号也统计出来,这是一个问题,原本的文中并没有这些符号但是却把它们统计出来了。

解决方法:

检查代码后发现原来是代码中的正则表达式有问题

修改前:

        partten = '([\u4e00-\u9fa5]|<li>|</li>)+'

修改后:

        partten = '([\u4e00-\u9fa5])+'  # 匹配中文

拼音流切分

拼音流切分指的是根据中文文本中的拼音音节将文本切分成适当的部分。这种方法通常用于文本处理和语言处理任务,特别是对于一些需要按照拼音音节进行处理的应用,比如拼音输入法或者文本转换为拼音的需求。通过将中文文本按照拼音音节进行切分,可以更方便地处理文本,并且为一些自然语言处理任务提供便利,比如文本分词、拼音转汉字等。

class pinyin(object):
    def __init__(self, pinyins):
        self.pinyins = pinyins# 要进行分割的拼音串
        # 读入所有有效拼音
        self.tree = Trie()
        f = open('pinyin/pinyin_list.txt')
        # f = open('pinyin_list.txt')
        for line in f:# 逐行读入
            self.tree.insert(line.split()[0])# 插入到Tree树中
        f.close()# 关闭文件

    def split(self):# 分割函数
        '''
        分割函数
        @param pinyin:  拼音串 str
        @return:        分割后的拼音列表 list
        '''
        # 可作为拼音开头的字母
        pinyin_initials = ['a', 'b', 'e', 'p', 'm', 'f', 'd',
                           't', 'n', 'l', 'g', 'k', 'h', 'j',
                           'q', 'x', 'r', 'z', 'c', 's', 'y', 'w']# 读入所有有效拼音
        # pinyin_initials = self.tree.root.children
        iuv = ['i','u','v']# i|u|v
        grn = ['g','r','n']# g|r|n

        input = ''# 输入拼音串
        result = []# 分割结果

        for i in range(len(self.pinyins)):# 逐个读入拼音
            c = self.pinyins[i]# 读入字符 c
            # 读入字符 c
            input += c# 将字符 c 加入到输入拼音串中
            # c是 i|u|v,并且是拼音串的首字母
            if c in iuv and len(input)==1:# 读入字符 c
                return False,None# 返回错误
            # 当前拼音有效或者是有效拼音的一部分
            if self.tree.find_initial_with(input):# 读入字符 c
                continue# 继续读入
            # c是声母
            if c in pinyin_initials:# 读入字符 c
                # 前面的拼音为有效拼音
                if self.tree.find_initial_with(input[:-1]):# 读入字符 c
                    # 在c前断开
                    result.append(input[:-1])# 将c前的拼音加入到分割结果中
                    input = input[-1:]# 将c加入到输入拼音串中
                    continue# 继续读入
                else:# 读入字符 c
                    return False,None# 返回错误
            # 倒数第二个字母为 g|r|n
            elif input[-2:-1] in grn:# 读入字符 c
                # 在 g|r|n 前断开有效
                if self.tree.find_initial_with(input[:-2]):# 读入字符 c
                    # 在 g|r|n 前断开
                    result.append(input[:-2])# 将 g|r|n 前的拼音加入到分割结果中
                    input = input[-2:]# 将 g|r|n 加入到输入拼音串中
                    continue# 继续读入
                # 在 g|r|n 后断开有效
                elif self.tree.find_initial_with(input[:-1]):# 读入字符 c
                    # 在 g|r|n 后断开
                    result.append(input[:-1])# 将 g|r|n 后的拼音加入到分割结果中
                    input = input[-1:]# 将 g|r|n 加入到输入拼音串中
                    continue# 继续读入
            else:# 读入字符 c
                # 单独断开
                result.append(input)# 将当前拼音加入到分割结果中
                input = ''# 清空输入拼音串

        result.append(input)# 将最后一个拼音加入到分割结果中

        return True,result# 返回分割结果

这个 pinyin Python 类用来处理和分割汉语拼音字符串。该类的split方法,可以根据一系列规则和存储在字典树(Trie)数据结构中的有效拼音列表,将给定的拼音字符串分割成单独的拼音组件。下面是对其功能的解释:

  1. 初始化(__init__): 构造函数接受一个拼音字符串(pinyins),并初始化一个 Trie 对象。然后,它从文件中读取有效拼音的列表,并将它们插入到字典树中,以便以后进行验证。
  2. 分割方法(split): 这是该类的核心功能。它会根据拼音语法规则将提供的拼音字符串分割成单独的组件。该方法使用拼音声母列表和某些汉语音韵学规则来确定有效的分割点。
  3. 特殊情况处理: 该方法包括对某些条件的特殊检查,例如:
    • 如果字符串以 ‘i’、’u’ 或 ‘v’ 开头,则拒绝该字符串,因为这些不是拼音中有效的起始字母。
    • 处理当前字符是拼音声母的情况,并确定是否在此处分割。
    • 对以 ‘g’、’r’ 或 ‘n’ 结尾的拼音组件进行特殊处理。
  4. 验证和分割逻辑: 在整个方法中,使用字典树结构来验证输入的子字符串是否是有效的拼音组件。该方法尝试根据拼音规则和输入字符串的不同断点。
  5. 返回值: 该方法返回一个元组,其中第一个元素是一个布尔值,表示成功或失败,如果成功,第二个元素是分割的拼音组件列表。

HMM建议中文输入法

这里我们使用了Viterbi算法来解决HMM建议中文输入法的问题

Viterbi算法

Viterbi算法是一种动态规划算法,常用于解决与隐马尔可夫模型(Hidden Markov Models, HMMs)相关的问题,特别是解码问题,即在给定观测序列的情况下找到最可能的状态序列。该算法由Andrew Viterbi于1967年提出,并广泛应用于数字通信和信号处理领域,后来也被用于自然语言处理和计算生物学等领域。

基本原理

在HMM中,有两种序列:观测序列(即我们可以直接观察到的事件序列)和状态序列(隐藏在背后,不能直接观察,但影响观测序列的生成)。Viterbi算法的目标是根据观测序列推断出最有可能生成这些观测的状态序列。

工作原理
  1. 初始化: 对于第一个观测,计算达到每个状态的最大概率,并记录下来。这些概率是初始状态概率与从初始状态到当前观测的发射概率的乘积。
  2. 递推: 对于每个后续的观测,计算从任一先前状态转移到当前状态的最大概率,并乘以当前状态到当前观测的发射概率。这一步被称为“向前递推”。
  3. 路径回溯: 在处理完所有观测后,算法找到最终观测的最大概率状态。然后,从这个状态开始回溯,找到达到这个状态的最佳路径,直至回到起点。
  4. 输出: 输出的是整个观测序列对应的最可能状态序列。
性能和优点

Viterbi算法相比于穷举搜索所有可能的状态序列,效率更高,因为它利用了动态规划原理,避免了重复计算,并在每步只保留最优的路径。这使得它在处理大规模数据时更为有效和实用。

限制
  • 标记假设: 算法假设未来的状态只依赖于当前状态(马尔可夫性质),这可能限制其在处理复杂依赖关系时的有效性。
  • 资源消耗: 在处理非常大的状态空间时,Viterbi算法可能会消耗大量计算资源。
代码实现
class InputMethod(object):
  def __init__(self):
    # 加载语言模型
    self.lm = LanguageModel()
    # 待求解的拼音输入
    self.pinyins = []
    # 有向图
    self.graph = None
    # viterbi递归的缓存
    self.viterbi_cache = {}

  def get_key(self, *words):
    return '_'.join([ str(w) for w in words])

  def translate(self, pinyins):
    '''
    @param pinyins: 拼音列表
    @return:        汉字串
    '''
    self.graph = Graph(pinyins, self)# 有向图
    self.viterbi_cache = {}# viterbi递归的缓存
    self.pinyins = pinyins# 拼音列表

    # 从第一个字开始使用viterbi算法求解最大路径
    words = self.graph.sequence[0].keys()# 第一个字的所有可能词
    max_node = None# 最大概率的词
    max_score = 0.0# 最大概率
    for k in words:# 遍历第一个字的所有可能词
      node = self.graph.sequence[0][k]
      score = self.viterbi(0, k)
      if score > max_score:
        max_score = score
        max_node = node

    # 输出中文路径
    result = []
    while True:# 遍历所有拼音
      #TODO:实现如果搜不到词汇,能输出最大概率的词而不是什么也不输出
      if(max_node is None):# 遍历所有拼音
        print("没有查找到汉字请输入正确的拼音")#
        break
      result.append(max_node.word)
      if not max_node.next_node:
        break
      max_node = max_node.next_node

    print (' '.join(pinyins))
    return (''.join(result))

  def viterbi(self, t, k):
    '''第 t 个位置出现 k 词的概率

    @param t:   pinyin数组下标
    @param k:   词
    @return:    最大分值
    '''
    if self.get_key(t,k) in self.viterbi_cache:
      return self.viterbi_cache[self.get_key(t,k)]

    node = self.graph.sequence[t][k]
    # 当前词长度
    length_self = len(k)
    # 开始时加载句首词词频作为初始概率
    if t == 0:
      init_prop = self.lm.get_init_score(k)
    else:
      init_prop = 1

    # 到达结尾
    if t == len(self.pinyins)-length_self:
      pinyin = '|'.join(self.pinyins[t:t+length_self])
      emission_prop = 1/self.lm.emission[pinyin][k]

      node.max_score = emission_prop
      self.viterbi_cache[self.get_key(t,k)] = node.max_score
      return node.max_score

    # 获得下一个状态所有可能的词
    next_words = self.graph.sequence[t+length_self].keys()
    for word in next_words:
      # 下一个词长度
      length_next = len(word)
      state_transfer = self.lm.get_trans_pro(word, k)
      pinyin = '|'.join(self.pinyins[t+length_self : t+length_self+length_next])

      emission_prop = 1/self.lm.emission[pinyin][word]
      # 递归调用,直到最后一个拼音结束
      score = self.viterbi(t+length_self, word) * state_transfer * emission_prop * init_prop

      if score > node.max_score:
        node.max_score = score
        node.next_node = self.graph.sequence[t+length_self][word]

    self.viterbi_cache[self.get_key(t,k)] = node.max_score
    return node.max_score

这段代码实现了一个中文输入法,它使用隐马尔可夫模型(HMM)和Viterbi算法将拼音串转换为汉字串。以下是对代码的详细分析:

InputMethod
  1. 初始化方法 __init__
    • self.lm = LanguageModel(): 加载一个语言模型实例。这个模型可能包含了状态转移概率、发射概率以及初始状态概率。
    • self.pinyins = []: 初始化一个列表来存储拼音输入。
    • self.graph = None: 初始化一个空的有向图,后续用于存储拼音到汉字的映射关系。
    • self.viterbi_cache = {}: 初始化一个字典,用作Viterbi算法的缓存,以提高效率。
  2. 辅助函数 get_key
    • 这个函数用于生成Viterbi缓存的键,将多个词合并为一个字符串。
  3. 翻译方法 translate
    • 接收一个拼音列表作为参数。
    • self.graph = Graph(pinyins, self): 根据拼音列表和当前输入法实例构建一个有向图。
    • self.viterbi_cache = {}: 重置Viterbi缓存。
    • 遍历图中第一个拼音对应的所有可能的汉字或词,使用Viterbi算法计算每个词的最大概率路径。
    • 选择概率最大的词作为起始点,并构建最终的汉字序列。
  4. Viterbi算法 viterbi
    • 计算在第t个拼音位置出现词k的最大概率。
    • 首先检查缓存中是否已有计算结果,如果有,直接返回。
    • 否则,计算该词的最大分值。
    • init_prop: 如果是序列的起始位置,则使用句首词的初始概率;否则设为1。
    • emission_prop: 计算发射概率,即在当前词状态下生成对应拼音的概率。
    • 遍历所有可能的下一个状态(词),递归调用Viterbi算法,并更新当前词的最大分值。
    • 将结果存入缓存。

出现的问题及解决办法

问题:

输入qidong输出却是’启东’而不是’启动’

解决办法:

根据debug发现时发射概率有问题,解决办法时先对self.freq进行排序这样就会使得输出是 启动

对 AllenNLP 代码的理解

我们可以先对AllenNLP进行一个宏观的然是AllenNLP是一个基于PyTorch的Apache 2.0开源自然语言处理(NLP)研究库。它旨在帮助研究人员开发最先进的深度学习模型,并处理各种语言任务。该库提供了一系列预训练模型和支持各种NLP任务的模块。AllenNLP特别支持插件,可以动态加载自定义的类或额外的子命令。还提供了一个命令行界面(CLI),通过各种子命令如训练、评估和预测等来简化操作。

使用AllenNLP,可以进行多种高级自然语言处理任务和研究,具体包括但不限于以下方面:

  1. 文本理解与生成:构建模型来理解文本内容,生成文本或进行翻译。
  2. 情感分析:确定文本的情感倾向,比如判断用户评论是正面还是负面。
  3. 问答系统:创建能回答自然语言问题的系统。
  4. 文本摘要:自动生成文档的简短且有意义的摘要。
  5. 命名实体识别:从文本中识别和分类重要的元素,如人名、地点和组织。
  6. 关系抽取:从文本中识别实体之间的关系。
  7. 文本分类:对文本进行分类,如新闻文章、产品评论等。
  8. 语言模型:构建能够理解和生成自然语言的模型。

此外,AllenNLP提供了强大的功能,如:

  • 预训练模型:使用在大量数据上预训练的模型,可加快开发过程并提高性能。
  • 模块化设计:灵活的API允许您轻松地定制和扩展模型和组件,并且提供丰富的模块来构建模型,包括编码器、解码器、注意力机制等,同时支持自定义模型。
  • 插件支持:可以通过插件添加新功能或模型,使其易于扩展和适应新任务。
  • 实验管理:通过配置文件和CLI工具,轻松管理和复现实验。

总的来说,AllenNLP是一个功能强大的工具,适合从学术研究到产业应用的各种自然语言处理任务。

这里就稍微了解下他的注意力机制的代码实现

AllenNLP提供了一个灵活的框架来实现和使用注意力机制。在其库中,注意力通常作为一个模块(通常在modules/attention/目录下)实现,这样可以轻松地在不同的模型中重用和组合。

1. 注意力机制的实现:

  • 基类:所有的注意力模块都继承自一个基类(如Attention),这个基类定义了一些必要的接口和功能。这种设计使得添加新的注意力类型变得容易。
  • 多种注意力类型:AllenNLP实现了多种注意力机制,包括但不限于传统的点积注意力(Dot Product Attention)、缩放点积注意力(Scaled Dot Product Attention)、加性注意力(Additive Attention)和多头注意力(Multi-Head Attention)。这些实现提供了各种选项来探索不同的注意力策略。
  • 参数化:注意力模块通常是高度参数化的,允许用户指定维度大小、激活函数、缩放因子等。这些参数可以通过配置文件灵活地调整。

2. 使用注意力机制:

  • 集成到模型中:在定义模型时,可以将注意力模块作为组件集成进来。例如,在一个序列到序列的模型中,可以在解码器中使用注意力机制来关注编码器的输出。
  • 与其他组件协作:注意力通常与其他模块(如RNN、Transformer)一起使用。AllenNLP的模块化设计使得这种组合使用变得简单。
  • 配置和实验:通过配置文件,可以轻松地尝试不同的注意力机制和参数,快速进行实验和比较。

3. 使用注意力机制的方法代码示例:

pythonCopy code# 导入所需的模块
from allennlp.modules.attention import DotProductAttention, AdditiveAttention

# 初始化注意力模块
attention = DotProductAttention()

# 在模型中使用注意力
# 假设我们有query和key
attention_weights = attention(query, key)