🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋

文章目录

句子分割

(隐藏)马尔可夫模型

部分分割

词性标注

条件随机场

分块和句法分析

语言模型

递归神经网络

练习:字符 N-Gram

练习:词语言模型


到目前为止,我们将文档视为词袋。对于许多 NLP 任务来说,这是一种常见且易于实现的方法。但是,这种方法会产生低保真度的语言模型。单词的顺序对于语言中意义的编码和解码至关重要,为了结合这一点,我们需要对序列进行建模。

当人们在机器学习上下文中提到序列时,他们通常谈论的是数据序列,其中数据点并不独立于它们周围的数据点。我们仍然可以使用从数据点派生的特征,就像一般机器学习一样,但现在我们也可以使用来自附近数据点的数据和标签。例如,如果我们试图确定标记“produce”是用作名词还是动词,那么了解它周围的单词将非常有用。如果它前面的标记是“可能”,则表示“产生”是动词。如果“the”是前面的标记,则表明它是名词。这些其他词为我们提供了上下文。

如果“to”在“produce”之前呢?那仍然可以表示名词或动词。我们需要进一步回顾。这给了我们窗口的概念——我们想要捕获的上下文的数量。许多算法都有固定数量的上下文,它被确定为超参数。有一些算法,例如 LSTM,可以学习记住上下文的时间。

序列问题来自不同的领域和不同的数据格式。在本书中,我们使用的是文本数据,它是一系列字符、单词甚至消息。处理语音数据本质上是一个序列问题。经济学、物理学和医学也有很多序列问题。在这些不同的领域中,这些数据的统计数据和行为通常非常不同,但有许多共同的技术。

用于序列建模的常用技术之一是称为图形模型的一系列机器学习算法。图形模型可用于对概率结构进行建模。这使我们能够捕获上下文。在构建图形模型时,我们必须决定要捕获多少上下文。我们想要捕获多少上下文取决于问题的具体情况。还有一些模型可以让我们了解要使用多少上下文。学习图形模型所需的算法因所选模型而异。有些可以通过梯度下降来学习;其他人有自己的学习算法。

在本章中,我们将介绍一些流行的序列学习算法,例如隐马尔可夫模型、条件随机场和循环神经网络。让我们从句子分割和隐马尔可夫模型开始。

句子分割

中的一个最先学习的序列建模问题是句子边界 检测SBD问题,我们要在其中确定句子边界的位置。这似乎是一个可以通过正则表达式解决的问题。我们可以简单地说每个“.”、“?”和“!” 是句子边界。我们可以添加一些东西来检查首字母缩写词——也许我们可以检查前一个字符是否大写,而下一个空格后面的字符不是。这仍然会遗漏一些首字母缩略词的情况——例如,两个首字母缩略词彼此跟随,如“USDOS”

如果我们的文本格式不那么规则怎么办?如果我们的文本有很多列表,这在技术交流中很常见,那么每个项目的末尾可能没有句尾标点符号。但是,我们不想将列表中的所有项目都称为同一个句子。我们可以制作越来越复杂的正则表达式,或者我们可以建立一个模型。

有一些优点和缺点可以帮助我们决定何时对 SBD 使用正则表达式以及何时使用模型。如果忽略一些异常,使用正则表达式很容易。如果您遇到难以在文本上使用模型的情况,最好只使用正则表达式。另一方面,SBD 模型具有一定的泛化性。话虽如此,如果您使用建立在报纸上的 SBD 模型来处理临床文本,它会产生比您预期的更多的错误。

(隐藏)马尔可夫模型

隐马尔可夫模型HMM是处理序列数据的流行模型。为了理解它,我们需要讨论马尔可夫模型和马尔可夫属性。

马尔可夫性质与随机(随机)过程有关。在随机过程中,时间的随机变量吨, X吨, 可以依赖于序列中所有先前的变量,X吨-1,X吨-2,...,X0. 如果X吨只依赖于前一个变量,X吨-1,那么我们说它具有马尔可夫性质。这大大简化了问题。这是语言中不切实际的假设。然而,就像我们在前一章看到的朴素贝叶斯和逻辑回归的独立假设一样,不切实际的假设不一定会产生一个糟糕的模型。我们也可以缓和马尔可夫性质并说X吨取决于最后一个ķ变量。

我们还可以使用我们希望建模的序列和可观察序列之间的关系。现在我们可以将特定状态的概率估计为:

磷[是一世=ķ|X一世=C]≈磷[是一世=ķ|是一世-1=ķ']·磷[是一世-1=ķ']·磷[X一世=C|是一世=ķ]

我们可以计算转移概率磷[是一世=ķ|是一世-1=ķ'],初始概率磷[是0=ķ], 和发射概率磷[X一世=C|是一世=ķ]如果我们有标签,则直接从数据中获取。一旦我们有了这个,我们如何预测隐藏状态,是一世? 我们使用维特比算法。

让我们看一个例子。我们将使用 NLTK 提供的布朗语料库。

from collections import defaultdict, Counter

import numpy as np
import pandas as pd

import sparknlp

spark = sparknlp.start()

from nltk.corpus import brown
sentences = brown.sents()

语料库已经被分割成句子,因此我们必须对数据进行去标记化以获得训练数据。幸运的是,这也为我们提供了标签。我们将用 标记句子的最后一个字符,E其他所有字符都将标记为S

def detokenize(sentence):
    text = ''
    for token in sentence:
        if text and any(c.isalnum() for c in token):
            text += ' '
        text += token
    return text
word_counts = Counter()
raw = []
labels = []
for fid in brown.fileids():
        sentences = brown.sents(fid)
        word_counts.update(
            [t for s in sentences for t in s if t.isalpha()])
        sentences = [detokenize(s) for s in sentences]
        raw.append(' '.join(sentences))
        labels.append('S'.join([
            ('S' * (len(s) - 1)) + 'E' 
            for s in sentences
        ]))

word_counts = pd.Series(word_counts))

现在,让我们定义我们的训练算法。如果你注意到前面定义的方程,我们将重复概率乘法;这会产生下溢的危险。下溢是浮点数的限制。浮点数只能表示这么多的精度。因此,如果一个数字过于接近 0,浮点表示可能会舍入为 0。例如,在实现朴素贝叶斯时,我们将添加对数概率,而不是乘以概率。这是为了避免在乘以许多小于 1 的数字时下溢。

我们将需要观察集、文本中的字符、状态集、字符是否标记句子结尾以及日志概率。我们将需要初始状态的对数概率。在这个建模问题中,初始状态总是“S”。我们将需要发射对数概率——给定状态的字符的对数概率。最后,我们需要转换对数概率,它是给定前一个状态的状态的对数概率。

class HMM(object):
    def __init__(self, obs_set, state_set, initial_log, emission_log, 
                 transition_log):
        self.obs_set = obs_set
        self.state_set = state_set
        self.initial_log = initial_log
        self.emission_log = emission_log
        self.transition_log = transition_log

为了计算这些东西,我们需要跟踪所有可能的观察结果、状态以及我们将用于计算对数概率的计数。

def training_data():
    data_dict = {}
    data_dict['obs_set'] = set()
    data_dict['state_set'] = set()
    data_dict['transition_ct'] = defaultdict(Counter)
    data_dict['emission_ct'] = defaultdict(Counter)
    data_dict['initial_ct'] = Counter()
    return data_dict

现在我们需要一个函数来在我们遍历数据集时更新这些数据。

def update_state(data_dict, ob_seq, st_seq):
    assert len(ob_seq) == len(st_seq)
    data_dict['initial_ct'][st_seq[0]] += 1
    for i in range(1, len(st_seq)):
        ob = ob_seq[i]
        st = st_seq[i]
        data_dict['obs_set'].add(ob)
        data_dict['state_set'].add(st)
        data_dict['transition_ct'][ob_seq[i-1]][ob] += 1
        data_dict['emission_ct'][st][ob] += 1

现在我们有了计数和观察和状态的总集合,我们可以计算对数概率所需的总和。

def calculate_sums(data_dict):
    data_dict['transition_sums'] = {
        st: np.sum(list(data_dict['transition_ct'][st].values())) 
        for st in data_dict['state_set']
    }
    data_dict['initial_sum'] = np.sum(
        list(data_dict['initial_ct'].values()))
    data_dict['emission_sums'] = {
        st: np.sum(list(data_dict['emission_ct'][st].values())) 
        for st in data_dict['state_set']
    }

一旦我们有了计数和总和,我们就可以计算对数概率。

def calculate_log_probs(data_dict, eps):
    data_dict['transition_log'] = {
        prev_st: {
            # log P[y_i = k | y_i-1 = k']
            st: (np.log(data_dict['transition_ct'][prev_st][st] + \
                        eps) - \
                 np.log(data_dict['transition_sums'][prev_st] + \
                        eps)) 
            for st in data_dict['state_set']
        } 
        for prev_st in data_dict['state_set']
    }
    
    data_dict['initial_log'] = {
            # log P[y_0 = k]
        st: (np.log(data_dict['initial_ct'][st] + eps) - \
             np.log(data_dict['initial_sum'] + eps)) 
        for st in data_dict['state_set']
    }
    
    data_dict['emission_log'] = {
        st: {
            # log P[x_i = c | y_i = k]
            ob: (np.log(data_dict['emission_ct'][st][ob] + eps) - \
                 np.log(data_dict['emission_sums'][st] + eps)) 
            for ob in data_dict['obs_set']
        } 
        for st in data_dict['state_set']
    }

最后,我们有了train将所有内容联系在一起的方法。

def train(observations, states, eps=1e-8):
    # initialize
    data_dict = training_data()
    
    # traverse data and count all transitions, initials, and 
    # emissions
    for ob_seq, st_seq in zip(observations, states):
        update_state(data_dict, ob_seq, st_seq)
                
    calculate_sums(data_dict)
    
    calculate_log_probs(data_dict, eps)
    
    return HMM(list(data_dict['obs_set']), list(data_dict['state_set']), 
               data_dict['initial_log'], data_dict['emission_log'], 
               data_dict['transition_log'])
model = train(raw, labels)

现在,给定一段文本,我们需要计算最可能的状态序列。我们可以使用这些预测状态将一段文本拆分为句子。为此,我们将使用维特比算法。该算法将让我们有效地遍历一组可能的序列。

def viterbi(y, model):
    # 初始状态的概率
    path_logs = [{
        st: model.initial_log[st] + model.emission_log[st][y[0]] 
        for st in model.state_set
    }]
    path_preds = [{st: '' for st in model.state_set}]
    
    for i in range(1, len(y)):
        curr_log = {}
        curr_preds = {}
        for st in model.state_set:
            # 找到最可能的先前状态
            # 将导致 st
            curr_log[st] = -np.inf
            curr_preds[st] = ''
            for prev_st in model.state_set:
                # log probability
                local_log = path_logs[i-1][prev_st] + \
                    model.transition_log[prev_st][st] + \
                    model.emission_log[st][y[i]]
                if curr_log[st] < local_log:
                    curr_log[st] = local_log
                    curr_preds[st] = prev_st
        path_logs.append(curr_log)
        path_preds.append(curr_preds)

    # 现在我们向后工作。找到最可能的最终
    # 状态,然后回到开头。
    terminal_log = -np.inf
    curr_st = ''
    for st in model.state_set:
        if terminal_log < path_logs[-1][st]:
            terminal_log = path_logs[-1][st]
            curr_st = st
    preds = curr_st
    for i in range(len(y)-1, 0, -1):
        curr_st = path_preds[i][curr_st]
        preds = curr_st + preds
    return preds

现在我们可以进行预测,我们可以构建自己的句子拆分器。

def split(text, model):
    state_seq = viterbi(text, model)
    sentences = []
    start = 0
    for end in range(1, len(text)):
        if state_seq[end] == 'E':
            sentences.append(text[start:end+1])
            start = end+1
    sentences.append(text[start:])
    return sentences

让我们看看它是怎么做的。

example = raw[0]

print('\n###\n'.join(split(example, model)[:10]))
The Fulton County Grand Jury said Friday an investigation of 
Atlanta's recent primary election produced`` no evidence'' that 
any irregularities took place.
###
The jury further said in term-
###
end presentments that the City Executive Committee, which had over-
###
all charge of the election,`` deserves the praise and thanks of the 
City of Atlanta'' for the manner in which the election was 
conducted.
###
The September-
###
October term jury had been charged by Fulton Superior Court Judge 
Durwood Pye to investigate reports of possible`` irregularities'' 
in the hard-
###
fought primary which was won by Mayor-
###
nominate Ivan Allen Jr.
###
.
###
`` Only a relative handful of such reports was received'', the jury 
said,`` considering the widespread interest in the election, the 
number of voters and the size of this city''.

不是很好,但这是一个从数据构建的简单模型,而不是手动编码的启发式方法。有几种方法可以改进这一点。我们可以添加更多发射特征,因为我们假设它们彼此独立。我们应该查看数据以了解为什么模型认为连字符是句子的结尾。

这个模型很简单,但我们已经有很多标签了。获得这样的标签可能是一个耗时的过程。您可以使用Baum-Welch算法来学习部分标记或未标记数据集的转移和发射概率。

让我们看看 Spark NLP 是如何做句子检测的。该算法基于 Kevin Dias 的pragmatic_segmenter,最初用 Ruby 实现。让我们将它与我们简单的 HMM 的作用进行比较。

example_df = spark.createDataFrame([(example,)], ['text'])
from sparknlp import DocumentAssembler, Finisher
from sparknlp.annotator import SentenceDetector

from pyspark.ml import Pipeline

assembler = DocumentAssembler()\
    .setInputCol('text')\
    .setOutputCol('document')
sent_detector = SentenceDetector()\
    .setInputCols(['document'])\
    .setOutputCol('sentences')
finisher = Finisher()\
    .setInputCols(['sentences'])\
    .setOutputCols(['sentences'])\
    .setOutputAsArray(True)

pipeline = Pipeline().setStages([
    assembler, sent_detector, finisher
]).fit(example_df)
sentences = pipeline.transform(example_df)

print('\n###\n'.join(sentences.first()['sentences'][:10]))
The Fulton County Grand Jury said Friday an investigation of 
Atlanta's recent primary election produced`` no evidence'' that 
any irregularities took place.
###
The jury further said in term-end presentments that the City 
Executive Committee, which had over-all charge of the election,`` 
deserves the praise and thanks of the City of Atlanta'' for the 
manner in which the election was conducted.
###
The September-October term jury had been charged by Fulton Superior 
Court Judge Durwood Pye to investigate reports of possible`` 
irregularities'' in the hard-fought primary which was won by 
Mayor-nominate Ivan Allen Jr..
###
`` Only a relative handful of such reports was received'', the jury 
said,`` considering the widespread interest in the election, the 
number of voters and the size of this city''.
###
The jury said it did find that many of Georgia's registration and 
election laws`` are outmoded or inadequate and often ambiguous''.
###
It recommended that Fulton legislators act`` to have these laws 
studied and revised to the end of modernizing and improving them''.
###
The grand jury commented on a number of other topics, among them 
the Atlanta and Fulton County purchasing departments which it 
said`` are well operated and follow generally accepted practices 
which inure to the best interest of both governments''.
###
Merger proposed However, the jury said it believes`` these two 
offices should be combined to achieve greater efficiency and reduce 
the cost of administration''.
###
The City Purchasing Department, the jury said,`` is lacking in 
experienced clerical personnel as a result of city personnel 
policies''.
###
It urged that the city`` take steps to remedy'' this problem.

它肯定比 HMM 做得更好。pragmatic_segmenter相当复杂。不过,我们可以建立一个更复杂的模型。这是一个很好的教训,在某些情况下,启发式可能比模型更可取。在处理 NLP 应用程序时,请始终先尝试最简单的解决方案。看看哪里出了问题,然后进行改进.

部分分割

有些文档更像是文档的集合。临床遭遇被记录为笔记的集合,可能来自不同的提供者。法律文本分为不同的部分,每个部分具有不同的内容和功能。叙事文本通常被分成章节或场景。这些不同的部分可能需要不同的处理,甚至不同的模型。例如,我们可能不想在录取通知书上使用与放射检查相同的模型,即使它们是同一访问的一部分。

尽管部分非常重要,但它们实际上并不是语言本身的一部分。它们是记录文本的文档格式的产物。在不同类型的部分中,甚至在它们的位置中,可能仍然存在意义。这意味着我们通常无法在给定的语料库之外概括我们的技术。幸运的是,正则表达式在这个问题上比句子边界检测更有效。

词性标注

词性是词类,它们控制词如何组合成短语和句子。这些可能非常有价值,尤其是在涉及从文本中提取信息的过程中。您可能熟悉最常见的词性。在 NLP 中,类别稍微复杂一些。

以下是常用的词性:

  • 动词:“know,” “try,” “call”
  • 名词:“cat,” “banana,” “happiness”
  • 形容词:
  • 副词:
  • 介词:“of”、“behind”、“with”

大多数词性标注数据来自宾夕法尼亚大学树库,或者是类似的格式。该数据集具有更大的词性集:

  • CC:并列连词(“and”)
  • CD:基数(“one”、“1”)
  • DT:限定词(“an”、“the”)
  • EX:存在的“there”(“there are”)
  • FW:外来词(“zeitgeist”)
  • IN:介词或从属连词(“of”、“because”)
  • JJ:形容词(“happy”、“fun”)
  • JJR:形容词,比较(“happier”)
  • JJS:形容词,最高级(“happiest”)
  • LS:列表项标记(“a)”)
  • MD:模态(“can,” “might”)
  • NN:名词、单数或整体
  • NNS:名词,复数
  • NNP:专有名词,单数(“Sarah”)
  • NNPS:专有名词,复数
  • PDT:预定者(“It is half the price”中的“half”。)
  • POS:所有格结尾(所有格“'s”)
  • PRP:人称代词
  • PRP\$:所有格代词
  • RB:副词(“quickly,” “well”)
  • RBR:副词,比较
  • RBS:副词,最高级
  • RP:粒子(不同,但在“It's a write-off”中不定式“to”、“off”)
  • SYM:符号(数学上下文中的 x)
  • TO:to(有时是一个单独的类别,仅用于不定式“to”)
  • 呃:感叹词(“uh”)
  • VB:动词,基本形式(在不定式“to”、“call”、“know”之后)
  • VBD:动词,过去时
  • VBG:动词、动名词或现在分词
  • VBN:动词、过去分词(“called,” “known”)
  • VBP:动词,非第三人称单数现在时
  • VBZ:动词,第三人称单数现在时(“calls,” “knows”)
  • WDT:Wh-determiner(“which”)
  • WP:Wh-代词(“who”)
  • WP\$:所有格 wh 代词
  • WRB:Wh-副词(“when”)

了解这些词汇类别背后的语言学将有助于我们了解如何提取它们,以及如何使用它们。让我们看一下人类如何识别词性。

人类从形态和句法线索中解码词性。这就是为什么我们可以确定无意义词的词性。让我们看看刘易斯卡罗尔的诗“Jabberwocky”的一部分。

’Twas brillig, and the slithy toves
Did gyre and gimble in the wabe:
All mimsy were the borogoves,
And the mome raths outgrabe.

“Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!”

He took his vorpal sword in hand;
Long time the manxome foe he sought—
So rested he by the Tumtum tree
And stood awhile in thought.

说流利的英语的人会告诉你“brillig”和“vorpal”是形容词,“gyre”和“gimble”是动词,“toves”和“Jabberwock”是名词。对每个类别都做到这一点并不容易。如果你自己编了从属连词,人们可能很难识别它。

I went there cloom they told me to.

这句话似乎是错误的。原因是我们习惯于在某些类别中学习新词,而不是在其他类别中。如果我们可以在一个类别中创建单词,它被认为是一个开放的类别;那些我们不能轻易添加的被称为封闭类别。不过,这是在一个范围内。代词不像名词那样开放,但语言创新新代词的情况并不少见。例如,“你们”的历史不超过两三个世纪。

知道有些类别或多或少是固定的,有些类别是完全开放的,我们可以看出我们的模型正在学习两种预测。词汇提示对封闭类别有用,上下文提示对开放类别有用。

下面我们来看看 Spark NLP 是如何做 POS 标签的。在 Spark NLP 中,使用了感知器。我们可以在棕色语料库上训练模型,但首先我们必须以特定格式保存数据。每个标记-标签对必须由下划线“_”连接。每个句子的标记标记都放在一行上。例如:

The_AT mayor's_NN$ present_JJ term_NN of_IN office_NN expires_VBZ 
Jan._NP 1_CD ._. 
He_PPS will_MD be_BE succeeded_VBN by_IN Ivan_NP Allen_NP Jr._NP 
,_, who_WPS became_VBD a_AT candidate_NN in_IN the_AT Sept._NP 
13_CD primary_NN after_CS Mayor_NN-TL Hartsfield_NP announced_VBD 
that_CS he_PPS would_MD not_* run_VB for_IN reelection_NN ._.
from sparknlp.training import POS

with open('tagged_brown.txt', 'w') as out:
    for fid in brown.fileids():
        for sent in brown.tagged_sents(fid):
            for token, tag in sent:
                out.write('{}_{} '.format(token, tag))
            out.write('\n')
        
tag_data = POS().readDataset(spark, 'tagged_brown.txt', '_', 'tags')

现在我们可以构建我们的管道并训练我们的模型。

from sparknlp.annotator import Tokenizer, PerceptronApproach

assembler = DocumentAssembler()\
    .setInputCol('text')\
    .setOutputCol('document')
sent_detector = SentenceDetector()\
    .setInputCols(['document'])\
    .setOutputCol('sentences')
tokenizer = Tokenizer() \
    .setInputCols(['sentences']) \
    .setOutputCol('tokens')

pos_tagger = PerceptronApproach() \
    .setNIterations(1) \
    .setInputCols(["sentences", "tokens"]) \
    .setOutputCol("pos") \
    .setPosCol("tags")

finisher = Finisher()\
    .setInputCols(['tokens', 'pos'])\
    .setOutputCols(['tokens', 'pos'])\
    .setOutputAsArray(True)

pipeline = Pipeline().setStages([
    assembler, sent_detector, tokenizer, pos_tagger, finisher
])
pipeline = pipeline.fit(tag_data)

让我们看看它在第一句话中的表现。

tag_data.first()['text']
'The Friday an investigation of primary election produced evidence 
any irregularities took place .'
tag_data_first = tag_data.first()['tags']
txformed_first = pipeline.transform(tag_data).first()

for i in range(len(tag_data_first)):
    word = tag_data_first[i]['metadata']['word']
    true_pos = tag_data_first[i]['result']
    pred_pos = txformed_first['pos'][i]
    print('{:20s} {:5s} {:5s}'.format(word, true_pos, pred_pos))
The                  AT    AT   
Friday               NR    NR   
an                   AT    AT   
investigation        NN    NN   
of                   IN    IN   
primary              NN    JJ   
election             NN    NN   
produced             VBD   VBN  
evidence             NN    NN   
any                  DTI   DTI  
irregularities       NNS   NNS  
took                 VBD   VBD  
place                NN    NN

该模型已经很好地学习了这个数据集。事实上,“主要”作为名词或形容词都可以是真的,这取决于你如何解析句子。

还有其他用于词性标记的技术,例如条件随机场

条件随机场

如果隐马尔可夫模型是顺序朴素贝叶斯,那么条件随机场 (CRF) 可以被认为是顺序逻辑回归。CRF 是另一种流行的词性标注技术。与 HMM 相比,CRF 有一些好处。它们允许更复杂的特征,因为它们对特征和标签之间的关系做出的假设更少。在 Spark NLP 中,除了使用 RNN 之外,CRF 是用于词性标注的一种方法。CRF 是使用逻辑回归等梯度下降来学习的,但它们是使用Viterbi算法等算法运行的。

分块和句法分析

现在我们已经有了各个标记的词性,我们必须考虑将它们组合起来。许多 NLP 任务涉及在文本中查找实体。通常,这些实体使用多个单词的短语来引用。这意味着我们必须了解如何在短语中组合标记的标记,通常称为块。

与句子边界检测类似,我们可以使用启发式方法来做到这一点,但是会出现需要模型来避免的错误。启发式方法相对简单——如果两个相邻的标记具有相同或相似的标签,则将它们组合成一个短语。例如,“扇叶”这两个词都是名词,因此可以将它们组合成一个名词短语。我们可以将某些已知结构(例如某些动词介词组合,例如“knock off”)组合成单个动词或名词。启发式不会涵盖更复杂的句法结构。例如,动词“knock off”在“knock”和“off”之间插入它的宾语,所以如果你想规范你的词汇,不定式“to knock off”不会与变形形式“knocked X”结合使用离开。” 您可能还对某些句法结构感兴趣,

为了提取这些更复杂的句法结构,我们需要一个句法解析器。这些模型将序列转换为树结构。然后这些标记成为更大短语中的组成部分。语言非常复杂,但幸运的是,简单的结构通常比复杂的结构更常见。

在使用句法解析器之前,请确保您确定自己需要它。它们通常是复杂的模型,训练和使用需要大量资源。最好先尝试用启发式方法解决问题,如果这还不够,请考虑使用句法解析器。

关于句法解析器的另一个警告是标记是一项艰巨的任务。任何精通一门语言的人都可以可靠地将文本拆分成句子。大多数人可以在几分钟内很好地学习词性以标记数据。句法解析是一项复杂得多的标记任务。

语言模型

序列建模的另一个经典应用是语言建模。语言模型是生成语言的过程模型。这被称为生成模型,与用于区分事物之间差异的判别模型相反。当然,生成人类语言的实际过程非常复杂,神经学家、心理学家和哲学家仍在探索。在 NLP 中,语言模型做出了简化的假设——例如,文本生成过程可以仅从文本中学习。

语言模型有很多用途。我们可以将它们用于其他模型的特征生成。例如,基于神经网络的语言模型可用于输入用于序列标记的其他层。语言模型也用于文本生成和摘要。

本章介绍的一些技术可用于创建语言模型。例如,我们可以使用 CRF 来预测单词序列。我们还可以使用马尔科夫模型通过学习转移概率来预测单词序列。这不会是一个隐藏的马尔可夫模型,因为这里没有隐藏状态。我们可能需要一个比前一个标记更大的上下文窗口。幸运的是,我们可以放宽语言生成具有马尔可夫性质的假设。然而,一旦我们开始这样做,我们的模型很快就会变得更加复杂。这与使语法分析器如此具有挑战性的语法复杂性有关。

目前,RNN(循环神经网络)是构建语言模型的最流行方法。我们在第 4 章介绍了 RNN ,但现在让我们更详细地了解它们是如何工作的。

递归神经网络

我们将建立一个可以生成英语单词的模型。为此,我们将使用 LSTM。以下是定义它的方程式:

spark 训练模型 spark建模_keras

LSTM 背后的想法是,它会在您完成序列时保持状态。它将通过训练过程学习何时更新其状态。输入,X吨, 与之前的状态相结合,在吨,使用四组不同的权重。第一组,在F,在F,bF,代表遗忘,控制有多少先验信息影响细胞的状态。第二组,在一世,在一世,b一世, 表示单元格的输入,控制当前示例对单元格状态的影响程度。第三组,在○,在○,b○,表示单元的输出,控制新的单元状态对 LSTM 输出的影响程度。最后,在C,在C,bC, 表示当前状态的记忆,控制从当前输入中记忆并存储在单元格中的内容。

为了传递我们的字符,我们首先需要对它们进行矢量化。

from keras.models import Model, Sequential
from keras.layers import *
import keras.utils as ku
import keras.preprocessing as kp

from scipy.special import expit as sigmoid, softmax

让我们只选择经常出现的单词。此外,我们将标记每个单词的结尾。这将使我们的模型预测单词何时结束。

vocab = word_counts[word_counts > 100]
vocab = list(w + '#' for w in vocab.index)

我们将构建两个查找,c2i用于将字符映射到索引,以及i2c将索引映射回字符。我们还将?用作未知字符的符号。

UNK = '?'
c2i = {UNK: 0}

for word in vocab:
    for c in word:
        c2i.setdefault(c, len(c2i))
        
i2c = {ix: c for c, ix in c2i.items()}
alphabet_size = len(i2c) + 1

现在让我们定义一些用于转换数据的实用函数。

def texts_to_sequences(texts):
    return [[c2i.get(c, c2i[UNK]) for c in t] for t in texts]

def sequences_to_texts(seqs):
    return [''.join([i2c.get(ix, UNK) for ix in s]) for s in seqs]

在这里,我们将最大上下文指定为 10。我们可能不会这样做,但这可能会导致一些技术困难。序列建模的实现期望知道序列的最大长度。在不固定窗口大小的情况下,最长单词的长度将决定这个长度。因为长词比短词少得多,所以我们的大部分序列都需要填充。这种填充并不能帮助我们学习可能的序列。所以有一个权衡:窗口越大,模型预测序列中下一个项目的上下文就越多。另一方面,它也增加了计算复杂度。最好实际考虑数据中上下文的大小。在英语中,词汇的长度中位数是 6 个字母。如果考虑词频,中位数为 4。事实上,考虑到词频,10 是词长的第 95 个百分位数。这意味着来自超过 10 个字符的信息很少有助于预测下一个字符。

seqs = texts_to_sequences(vocab)

w = 10
X = []
Y = []
for seq in seqs:
    for k in range(1, min(w, len(seq))):
        X.append(seq[:k])
        Y.append(seq[k])
    for k in range(0, len(seq) - w):
        X.append(seq[k:k+w])
        Y.append(seq[k+w])
X = kp.sequence.pad_sequences(X, maxlen=w, padding='pre')
Y = ku.to_categorical(Y, num_classes=alphabet_size)

现在我们建立我们的模型。您可能会注意到Embedding这里的图层。这将减少我们输入的维度。LSTM 输入的宽度不是我们的字母表的大小,而是 5。我们将在第 11 章了解更多关于嵌入的信息。

units = 20

model = Sequential()
model.add(Embedding(alphabet_size, 5, input_length=w))
model.add(LSTM(units, unroll=True))
model.add(Dense(alphabet_size, activation='softmax'))

print(model.summary())
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 10, 5)             250       
_________________________________________________________________
lstm_1 (LSTM)                (None, 20)                2080      
_________________________________________________________________
dense_1 (Dense)              (None, 50)                1050      
=================================================================
Total params: 3,380
Trainable params: 3,380
Non-trainable params: 0
_________________________________________________________________
None

有 3,380 个参数需要学习。不出所料,它们中的大多数都在 LSTM 中——我们网络中最复杂的部分。现在,让我们训练我们的网络。

model.compile(
    loss='categorical_crossentropy', optimizer='adam', 
    metrics=['accuracy'])
model.fit(X, Y, epochs=300, verbose=1)

Epoch 1/300
5688/5688 [==============================] - 1s 224us/step - 
  loss: 3.1837 - acc: 0.1790
Epoch 2/300
5688/5688 [==============================] - 0s 79us/step - 
  loss: 2.7894 - acc: 0.1834
...
Epoch 299/300
5688/5688 [==============================] - 0s 88us/step - 
  loss: 1.7275 - acc: 0.4524
Epoch 300/300
5688/5688 [==============================] - 0s 84us/step - 
  loss: 1.7267 - acc: 0.4517

<keras.callbacks.History at 0x7fbf5e94d438>

现在我们有了字符序列的模型,我们可以实际使用它来生成单词。我们所需要的只是一个种子角色。

def generate_word(seed_char, model):
    text = seed_char 
    for _ in range(100):
        # 编码当前文本
        encoded = texts_to_sequences([text])[0]
        # 填充 the sequence
        encoded = kp.sequence.pad_sequences( 
            [encoded], maxlen=w, padding='pre', truncating='pre')
        # 预测下一个索引
        pred = model.predict_classes(encoded, verbose=0) 
        # 转换索引
        pred = sequences_to_texts([pred])[0] 
        # 如果模型预测到单词的结尾,则退出
        if pred == '#': 
            break
        text += pred
    return text
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

for c in alphabet:
    print(c, '->', generate_word(c, model), end=', ')
    c = c.lower()
    print(c, '->', generate_word(c, model))

A -> And, a -> an
B -> Breng, b -> beation
C -> Court, c -> court
D -> Dear, d -> dear
E -> Englent, e -> exter
F -> Fer, f -> fort
G -> Gearth, g -> groud
H -> Her, h -> hort
I -> In, i -> indedant
J -> Jome, j -> jorter
K -> Kenglert, k -> kear
L -> Lexter, l -> land
M -> Mand, m -> mearth
N -> Not, n -> near
O -> One, o -> on
P -> Pling, p -> provest
Q -> Qexter, q -> quale
R -> Rhong, r -> rear
S -> Some, s -> state
T -> Ther, t -> ther
U -> Ued, u -> under
V -> Vexter, v -> vering
W -> Watere, w -> wate
X -> Xexter, x -> xowe
Y -> Yot, y -> yurn
Z -> Zexter, z -> zelous

这看起来很有趣。其中一些甚至是真实的词,例如“她”、“土地”和“国家”。用文字来做这件事的原则是一样的。维度的数量增加了,所以我们必须注意内存的使用。

让我们看看这些是如何工作的。首先,让我们提取我们的图层。

embedding = model.layers[0]
lstm = model.layers[1]
dense = model.layers[2]

在此之后,我们要提取权重。Keras 库不会单独存储 LSTM 层的每个权重,因此我们必须将它们拆分出来。

# 嵌入层没有偏置项,所以
# 我们在这里只得到一个
W_e = embedding.get_weights()[0]

W, U, b = lstm.get_weights()

# W_* 权重沿第二个轴连接
W_i = W[:, :units]
W_f = W[:, units: units * 2]
W_c = W[:, units * 2: units * 3]
W_o = W[:, units * 3:]

# U_*权重沿第二个轴连接
U_i = U[:, :units]
U_f = U[:, units: units * 2]
U_c = U[:, units * 2: units * 3]
U_o = U[:, units * 3:]

# b_* 权重也是串联的
b_i = b[:units]
b_f = b[units: units * 2]
b_c = b[units * 2: units * 3]
b_o = b[units * 3:]

# 最后输出权重
W_d, b_d = dense.get_weights()

让我们看看当我们尝试预测“recurren”之后的下一个字符时我们应该期待什么。

text = ['recurren']
encoded = texts_to_sequences(text) 
encoded = kp.sequence.pad_sequences(
    encoded, maxlen=w, padding='pre')
pred = model.predict_classes(encoded)
pred = sequences_to_texts([pred])
pred
['t']

这是有道理的,因为这会使这个词“反复出现”。现在,让我们看看我们是否可以自己计算一下。首先,我们必须创建我们的输入。

X = ['recurren']
X = texts_to_sequences(X)
X = kp.sequence.pad_sequences(encoded, maxlen=w, padding='pre')
X = np.eye(alphabet_size)[X.reshape(-1)].T
X.shape
(50, 10)

现在,我们可以使用嵌入层将我们的 50 维稀疏向量转换为更密集的 5 维向量。

V_e = np.dot(W_e.T, X).T
V_e.shape
(10, 5)

让我们通过 LSTM 运行它。这段代码大部分与前面的方程平行,唯一的例外是我们在h_*通过激活函数发送它们之前将值存储在变量中。这样做是为了防止代码行太长。

v_t = np.zeros(units)
c_t = np.zeros(units)
for v_e in V_e:
    h_f = np.dot(W_f.T, v_e) + np.dot(U_f.T, v_t) + b_f
    f_t = sigmoid(h_f)
    h_i = np.dot(W_i.T, v_e) + np.dot(U_i.T, v_t) + b_i
    i_t = sigmoid(h_i)
    h_o = np.dot(W_o.T, v_e) + np.dot(U_o.T, v_t) + b_o
    o_t = sigmoid(h_o)
    h_cc = np.dot(W_c.T, v_e) + np.dot(U_c.T, v_t) + b_c
    cc_t = np.tanh(h_cc)
    c_t = np.multiply(f_t, c_t) + np.multiply(i_t, cc_t)
    v_t = np.multiply(o_t, np.tanh(c_t))
    
v_t.shape
(20,)

我们将获取最后一个输出并将其通过密集层以得到我们的预测。

h_d = np.dot(W_d.T, v_t) + b_d
pred = softmax(h_d)
pred
array([5.82594437e-14, 1.42019430e-13, 6.24594676e-05, 7.96185826e-03,
       1.44256098e-01, 8.38904616e-02, 2.30058043e-03, 1.34377453e-02,
       2.41413353e-02, 8.99782631e-03, 3.62877644e-04, 7.10518831e-04,
       4.20883844e-05, 1.14326228e-01, 4.10492247e-01, 1.37839318e-03,
       1.71264211e-03, 1.74333516e-03, 2.45791054e-03, 3.24176673e-04,
       9.32490754e-05, 7.62545395e-14, 9.35015496e-05, 1.53205409e-01,
       2.67653674e-02, 1.24012713e-03, 5.49467572e-14, 3.55922084e-11,
       8.92650636e-14, 9.91368315e-14, 8.16121572e-14, 2.14432828e-18,
       9.12657866e-14, 3.24019025e-06, 9.51441183e-14, 8.55035544e-14,
       8.72065395e-14, 7.73119241e-14, 9.14113806e-14, 1.08231855e-13,
       3.22887102e-07, 8.59110640e-14, 1.10092976e-13, 8.71172907e-14,
       1.04723547e-13, 7.06023940e-14, 8.18420309e-14, 1.21049563e-13,
       8.37730240e-14, 1.04719426e-13])

我们只需要找到具有最高值的索引,所以我们使用argmax.

i2c[pred.argmax()]

't'

最后,我们有我们的预测。让我们看看一些亚军。

top5 = sorted(enumerate(pred), key=lambda x: x[1], reverse=True)[:5]

for ix, p in top5:
    print(i2c[ix], p)

t 0.5984201360888022e 0.12310572070859592 # 0.08221461341991843 c 0.043372692317500176 l 0.037207052073704866

我们可以推测为什么可以预测其他角色。字符“g”可以根据输入字符串与“recurring”的相似程度来预测。正如我们之前所讨论的,大多数单词都没有“recurren”那么长,因此预测单词的结尾也是有意义的。字符“c”是单词“recurrence”的一部分。而字符“s”是英文中的一个常用字母,所以经常出现的概率很高。

建模序列是现代 NLP 的核心部分。在词袋方法中,我们丢失了所有通过语法传达的信息。现在我们可以将语言建模为序列,我们可以看看如何从序列中提取信息。

练习:字符 N-Gram

修改语言模型 RNN 以使用字符 N-gram。所以,序列应该是recurrent -> 2-gram -> [re, cu, rr, en, t#]

请记住,您可能需要更新w以反映您需要多少上下文。

练习:词语言模型

以诗歌“Jabberwocky”为例,并以此构建语言模型。

  1. 您的序列将是诗中的台词——例如,[“'Twas brillig, and the sliphy toves”,“Did gyre and gimble in the wabe:”]。使用 Spark NLP 管道处理文本并提取标记列表。
  2. 您的编码代码将需要更改 , c2ii2c,texts_to_sequences并且sequences_to_texts需要更新以使用单词。
  3. generate_word将需要更新以生成行。