[Day5] 语言模型(二)-N-gram实作

一. 前言

前一天已经说明N-gram的一些计算方式了,这篇会以实作'预测词'来作为N-gram的范例,就是利用前面的词来预测後面该接哪个词较好,这是参与某堂课里面的其中一项作业,因觉得应用不错,故分享~但我有做一些小修改,让我比较好讲解XD~

二. 资料集

资料集是利用维基百科的资料的中文资料做处理,该课程提供的资料集较小,猜测应该是有缩小资料集,但来源是维基的没有错

三. 演算法

  1. 当前面无字时,使用 uni-gram 来显示第一个候选字的list
  2. 若前面有一个字以上,使用 bi-gram 来显示後一个候选字的list

四. 实作

  1. import 套件:

    import re
    from collections import Counter
    
  2. 简单的前处理,只保留中文,并断句:

    def prepocess_text(line: str) -> list:
        # 只保留中文字元,并且断开不连续的中文字
        chinese = r'[\u4E00-\u9FFF]+'
        segments = re.findall(chinese, line)
        return segments
    
  3. 读取data,用prepocess_text来做前处理:

    word_list = []
    with open('./wiki_zh_small.txt', encoding="utf-8") as file:
        for line in file.readlines():
            word_list += prepocess_text(line)
    
    • 例子:
    prepocess_text('“今天”雨会下非常大,大到你受不了')  
    # output: ['今天', '雨会下非常大', '大到你受不了']
    
  4. 建立counter的class,用来记录1个字出现几次,2个字出现几次:

    class etWordCounters:
        def __init__(self, n):
            self.n = n
            self.counters = [Counter() for _ in range(n + 1)]
    
        def generate_gram(self, segments):
    
            # 若 n=1->unigram n=2-> bigram
            for i in range(1, 1 + self.n):
                for segment in segments:
                    self.counters[i] += Counter(self._skip(segment, i))
    
            # 用 self.counters[0] 来记录总共有几个字,这边用'eating'当key XD
            self.counters[0] = Counter({'eating': sum(dict(self.counters[1]).values())})
    
        def __getitem__(self, k):
    
            return self.counters[k]
    
        def _skip(self, segment, n):
    
            if len(segment) < n:
                return []
            shift = n - 1
            for i in range(len(segment) - shift):
                yield segment[i : i + shift + 1]
    
  5. 建立N-gram 的class:

    class Ngram:
        def __init__(self, n: int, counters: list):
            """ 
            n: n-gram's n
            counters: etWordCounters object
            """
            self.n = n
            self.major_counter = counters[n]
            self.minor_counter = counters[n-1]
    
        def predict_next_word(self, prefix: str = '', top_k: int = 5):
            """
            explain: 
            if prefix is empty string, using 1-gram predict next word
            elif prefix is greater than one words use, using 2-gram
            """
            # 表示前无词可预测         
            if self.n <= 1:
                prefix = 'eating'
            else:
                prefix = prefix[-(self.n - 1):] # 取得str後面个字
    
            count_prefix = self.minor_counter[prefix]
            probs = []
            # get word and probablity         
            for key, count in dict(self.major_counter).items():
                # transfer eating to ''
                prefix = '' if prefix == 'eating' else prefix
                if key.startswith(prefix):
                    prob = count / count_prefix
                    probs.append((prob, key[-1]))
    
            sorted_probs = sorted(probs, reverse=True)
    
            return sorted_probs[:top_k] if top_k > 0 else sorted_probs
    
        def get_word_dict(self, prefix=''):
            """
            explain: 
            get predict_next_word result and transfer to dict
            """
            return {word: prob for prob, word in self.predict_next_word(prefix, top_k=-1)}
    
  6. 计算字的次数:

    • 例如 n=1是会显示:
      {我:5,
      很:4,
      帅:6,
      气:2}
      n=2则是显示:
      {我很: 3,
      很帅: 5,
      帅气:2}
    counters = etWordCounters(n=2)
    counters.generate_gram(word_list)
    
  7. 来看共有一个字:

    # 全部有多少字
    counters[0]
    # output: Counter({'eating': 371373}) 总共有371373个字
    
  8. 建立 uni-gram 与 bi-gram model

    unigram = Ngram(1, counters)
    bigram = Ngram(2, counters)
    
  9. 预测建立预测下个字model的class,若前面无词用uni-gram,有一个字以上用bi-gram:

    class ChineseWordPredict:
        def __init__(self, unigram, bigram):
            self.unigram = unigram
            self.bigram = bigram
    
        def predict_proba(self, prefix='', top_k=5):
            # 使用Ngram来建立选字系统
            if len(prefix) == 0:
                return self.unigram.predict_next_word(prefix, top_k)
            elif len(prefix) >= 1:
                return self.bigram.predict_next_word(prefix, top_k)
    
    model = ChineseWordPredict(unigram, bigram)
    
  10. 预测'我思'的下个字:

    probs = model.predict_proba('我思', top_k=4)
    probs
    
    # output: 
    # [(0.3370165745856354, '想'),
    #  (0.12154696132596685, '考'),
    #  (0.09944751381215469, '维'),
    #  (0.04419889502762431, '是')]
    

这次介绍怎麽建立N-gram 的模型,下一篇会开始介绍POS(part of speech) 以及如何用python实作演算法,POS的主题还蛮长的,应该会花个几天写这个主题~


<<:  [Day4] Arduino测试烧录

>>:  [Day 5] 排版布局Stack

Day13 SwiftUI 06 - WebView

接下来我们来看看在SwiftUI 怎麽使用WebView 网页的元件,SwiftUI 框架 有一个缺...

【後转前要多久】# Day23 JS - JavaScript 变数、运算

变数 JS目前有三种宣告变数的方法。 在ES5以前都用var,ES6之後推出let与const。 新...

客制化元件外观

ZK 元件的配色与设计都有经过设计师制定,但我想你仍有你自己特定的需要,当有需要改变元件外观时,有几...

第10天 - PHP新增MySQL资料表内容

延续昨天的文章,今天来做PHP新增资料表的内容(不影响资料表结构)。 注意! 跟资料库有关的动作 都...

19 - Remote - Containers - 在容器中开发

在正式开发前,开发者都需要安装许多软件,来建立开发的环境。但是安装的方式会因开发所在的机器环境而有所...