A véspera da revolução LLM da inteligência artificial: um modelo de transformador que varre o processamento de linguagem natural em um artigo

  • ATUALIZADO: em 27 de janeiro de 2023, este artigo chegou às manchetes do ATA. (Observação: o nome completo da ATA é Alibaba Technology Associate, que é a maior comunidade técnica do Alibaba Group)
  • ATUALIZADO: em 2 de fevereiro de 2023, este artigo foi apreciado por Lu Su no ATA. (Nota: Lu Su, cujo nome verdadeiro é Cheng Li, é sócio de Ali e CTO anterior do Ali Group)

Olá pessoal! Sou o capitão Mike. Atualmente trabalho no Grupo Alibaba como diretor/especialista sênior em operações abrangentes. Fui responsável pela equipe de produtos da indústria Taobao, vendas especiais de Tiantian e centro de operações de Dajuhuasuan. O nome de tela sempre foi "Capitão Mike". Depois de se formar na Universidade de Ciência e Tecnologia da China, ele trabalhou primeiro em tecnologia de streaming de áudio e vídeo, sistemas distribuídos, etc., trabalhou em Full Stack e, posteriormente, iniciou um negócio em tecnologia, produtos, operações, marketing, cadeia de suprimentos, etc. Depois de muitos anos, vim para Ali e liderei as equipes de produto e operação de diferentes empresas em Taoxi. O texto vem do meu blog pessoal: MikeCaptain - o blog de tecnologia, produto e negócios do Capitão Mike , combinando suas notas de estudo sobre a evolução técnica do modelo básico da PNL durante o Festival da Primavera e escrevendo no primeiro dia do Ano Novo Lunar em Hong Kong durante o Festival da Primavera. Este artigo contém 3 capítulos:

  • O primeiro capítulo apresenta principalmente vários modelos de linguagem mainstream antes do surgimento do Transformer , incluindo N-gram (n-gram), multi-layer perceptron (MLP), rede neural convolucional (CNN) e rede neural recorrente (RNN). Dentre elas, o principal campo de aplicação da CNN é na visão computacional, portanto não há um desenvolvimento mais detalhado. Outros modelos não são abrangentes e a principal consideração é entender e aplicar a partir da perspectiva de um aluno de campo, em vez de pesquisa.
  • O segundo capítulo é o núcleo deste artigo . Ele primeiro apresenta o Mecanismo de Atenção (Mecanismo de Atenção), e então com base na compreensão dos principais modelos de linguagem anteriores no primeiro capítulo, podemos entender melhor porque o Transformer tem um impacto revolucionário.
  • O terceiro capítulo é uma versão de implementação do Transformer , baseado no Tensorflow.

Durante o Festival da Primavera, além deste artigo, também separei um artigo sobre "Large Language Model (LLM) Evolution Review After Transformer" e um artigo sobre "LLM Leads the Productivity Revolution, Bringing Control of the Technology Pulse in the Next Anos", mas não tenho tempo para resolver a tipografia, e vou postar quando tiver tempo no futuro. Esses direitos devem ser hobbies técnicos para matar o tempo durante o Festival da Primavera. Porque são notas técnicas que são com viés de aprendizagem, todos são muito bem-vindos para criticar, corrigir e comunicar .

  • Autor: Zhong Chao (Capitão Mike)
  • E-mail: zhongchao.ustc#gmail.com (#->@)
  • WeChat: sinosuperman (especifique "empresa/organização, posição" para minha conveniência, obrigado)
  • Quando: 22 de janeiro de 2023

prefácio

Este artigo tenta esclarecer uma questão do ponto de vista técnico: Qual é a origem da explosão do AIGC no ano passado e do rápido desenvolvimento do campo de NLP (Natural Language Processing) nos últimos cinco anos?

Após esta pergunta ser respondida, haverá mais duas perguntas, mas este artigo não respondeu por enquanto: 1) Se for considerado que passar no teste de Turing representa AGI (Artificial General Intelligence, inteligência artificial geral), qual é o desenvolvimento atual de NLP e até mesmo AGI? 2) Qual pode ser a rota de desenvolvimento da AGI nos próximos anos?

Aproveitando o tempo do Festival da Primavera, escrevi uma nota tão longa com dezenas de milhares de palavras, espero que amigos com hobbies comuns possam lê-la e fazer correções.

1. Depois que vim para Ali, o primeiro novo hobby que adicionei foi "Modelos de Transformers", e o segundo novo hobby foi "Modelos de Transformers"

Escrevi uma fala tão fria, mas o que na verdade quero dizer é que o primeiro se refere aos modelos de brinquedos feitos à mão relacionados ao famoso IP "Transformers", e o segundo se refere ao Transformer, o modelo de linguagem de inteligência artificial que lidera o revolução. Esses dois hobbies não têm nenhuma ligação superficial e direta com o trabalho de e-commerce que estou fazendo atualmente, então devem ser considerados como hobbies.

Em 2022, a aplicação da "IA generativa" alcançou um rápido desenvolvimento. Como praticante da "Internet clássica", sinto profundamente as mudanças disruptivas que a tecnologia de IA pode trazer desta vez, o que me deixa animado e ansioso. No primeiro semestre de 2022, desde o responsável pelo negócio Tiantian Special Sale até o responsável pelo Dajuhuasuan Operation Center, tenho prestado atenção à proposta de modelo de transmissão ao vivo na plataforma de marketing por um longo tempo no ano passado, e tenho pensado em uma pergunta: o papel do comércio eletrônico ao vivo Eficiência (método de interpretação de produto mais adequado + direitos de domínio privado + compra por impulso, etc.) vs. ineficiência do comércio eletrônico de transmissão ao vivo (distribuição de transmissão ao vivo correspondência de mercadorias + milhares de pessoas na sala de transmissão ao vivo + status de produto desconhecido + âncora incontrolável, etc.), pode promover um modelo que retenha a alta eficiência da transmissão ao vivo e resolva a baixa eficiência da transmissão ao vivo ao mesmo tempo?

Há uma tonelada de coisas para explorar aqui, e essa não é a intenção original da série do Capitão Mike, mas é uma introdução ao motivo pelo qual estou começando a prestar muita atenção à IA. A base da tecnologia humana digital do comércio eletrônico de transmissão ao vivo inclui captura de movimento, simulação de expressão facial, renderização visual, geração de fala ao vivo, síntese de fala, etc. Com base nos primeiros princípios, descobri que, embora muitas tecnologias, como captura de movimento e renderização visual, ainda tenham grandes desafios, do ponto de vista comercial, o que realmente afeta a mente do usuário é a geração e interpretação da fala ao vivo, exceto para a âncora de cabeça. , a grande maioria dos bens de transmissão ao vivo está indo muito mal a esse respeito, então há um enorme conteúdo gerado por "aprendizado de máquina" que ultrapassa o espaço de mercado da maioria dos praticantes que não são cabeças, e isso depende completamente do processamento de linguagem natural ( NLP ).

Este problema pertence à categoria de "IA generativa". Os círculos científicos e tecnológicos estrangeiros o chamam de "Gen-AI", ou seja, IA generativa, e os círculos científicos e tecnológicos chineses o chamam de "AIGC", ou seja, Conteúdo Gerado por IA, que corresponde a UGC e PGC. O nome Gen-AI está mais relacionado ao assunto, especificamente o "modelo generativo de IA", que é um "motor de conteúdo". O nome chinês está mais preocupado com "aplicativo de conteúdo".

Por falar em AIGC, o familiar ChatGPT vai estrear no final de 2022. É também por causa da quebra do ChatGPT que a atenção do AIGC no círculo de tecnologia nacional disparou. Desde meados do ano passado, tenho prestado atenção ao código aberto Stable Diffusion, a estrela no campo de "Vensen graph, text2image", e então tenho prestado atenção à explosão de aplicativos text2image, incluindo Disco Diffusion, MidJourney, DALL E 2, etc., todos derivados de CV (visão computacional) A inovação técnica provocada pelo desenvolvimento do modelo Diffusion no campo.

As imagens geradas por IA são realmente incríveis. Eu amo mods de Transformers, e gosto muito de mechs, então gerei algumas fotos aleatoriamente, e postei aqui para todo mundo ver, a velocidade de criação é em minutos. (Observação: atualmente, as imagens geradas por IA são baseadas principalmente no desenvolvimento do aplicativo Diffusion, e o principal driver do texto gerado por IA é o modelo Transformer, que é mostrado apenas aqui)

Mas, do ponto de vista dos primeiros princípios, a amplitude de aplicação da geração de imagens é muito menor do que a da geração de texto. A essência do conteúdo do texto é a compreensão e geração de linguagem e caracteres. A história humana tem 6 milhões de anos, mas a história da civilização humana tem apenas cerca de 6.000 anos. A razão para o grande desenvolvimento da civilização nos últimos 2.000 anos é principalmente devido à invenção da escrita pelos humanos há mais de 3.500 anos. Portanto, a IA gerando texto significa que a IA pode colaborar de forma eficiente com os humanos de uma maneira familiar aos humanos (linguagem e escrita), o que certamente detonará uma revolução na produtividade. E isso afetará profundamente muitos campos, como comércio eletrônico, conteúdo, jogos, computação em nuvem e serviços corporativos.

2. Dominar a base técnica é a habilidade básica para entender o pulso da IA ​​no momento, e esse pulso guiará todas as esferas da vida

一旦深入关注 AI、关注 NLP 领域,你就会发现当下仍然处于一个技术发展突破的阶段,不关注技术的情况下来聊 AI、聊 NLP、聊 AIGC,那就只能是一个「爱好者」,而无法深入与这个行业内的弄潮儿对话,更不要提参与其中了。所以这个春节,麦克船长回归了当年做技术时的初心,翻了一些材料,学习了 NLP 语言模型的关键技术,在此作为技术学习笔记,与大家分享。尽管担心班门弄斧,但是本着费曼老师提倡的输出学习法,我把自己学习梳理的内容抛出来,除了会更帮助到我自己,也能结交一些对此同样在关注的同学们,欢迎感兴趣的同学加我的微信(微信号 sinosuperman)在业余时间和我交流。

阅读本文,先对你过往的基础知识做了一些假设,如果你暂未了解,可能在阅读时遇到以下内容做一些简单地查询即可:

  • Word Presentation:自然语言处理中的词表示法,主要涉及 embedding。
  • 张量:需要一点基础,比如了解张量的形状、升降维度等。但不会涉及到复杂问题,对一阶张量(向量)、二阶张量(矩阵)的简单运算有数学基础即可。对三阶张量,大概能想象出其空间含义即可。语言模型里理解词之间的距离,是有其空间几何意义的。
  • 技术框架:PyTorch 或 TensorFlow 框架。由于时间和篇幅关系,春节期间梳理这些时,对于框架基础,我主要是 Google 现用现查,询问 ChatGPT 以及在微信读书里直接搜索全文。

作为技术笔记难免有纰漏或理解错误,欢迎指正。文中自绘图片用的是 Graphviz,公式生成用的是 KaTeX,贴到 ATA 后难免有一些没有兼容的部分(发现的已做了 fix),望见谅。

第一章 · 2017 年之前的几个关键 NLP 语言模型

Em termos de base técnica da PNL, acho que são basicamente duas partes: representação de palavras (apresentação de palavras), modelo de linguagem (modelo de linguagem). Para o método de representação de palavras, não vou apresentá-lo em detalhes aqui. A ideia básica é representar as palavras como vetores (tensores unidimensionais), os mais básicos One-Hot, Word2Vec, GloVe, fastText, etc. A evolução técnica desta parte também está em constante avanço, por exemplo, no modelo Transformer que será abordado neste artigo, a palavra representação utilizada é "introdução de vetores de palavras sensíveis ao contexto".

O modelo de linguagem é desde o N-gram inicial (N-Gram, que será apresentado neste artigo), até o perceptron mais antigo (Perceptron) depois que a rede neural foi proposta e, em seguida, para a rede neural convolucional (CNN) e em seguida, uma rede neural recorrente (RNN, incluindo o modelo Encoder-Decoder) que considera características de sequência, até o Transformer, que nasceu em 2017, é dividido aproximadamente nesses cinco estágios principais. Como o foco deste artigo é o Transformer, darei uma visão geral rápida dos primeiros quatro modelos e, em seguida, apresentarei o mecanismo de atenção mais simples. Com base nisso, apresentarei o Transformer em detalhes e fornecerei um exemplo de código completo e refinado. palestras.

Seção 1 · Modelo de linguagem N-gram

1.1. Suposição de Markov e Modelo de Linguagem N-gram

A probabilidade da próxima palavra depende apenas das n-1 palavras anteriores. Essa suposição é chamada de "Suposição de Markov". N-gramas também são chamados de cadeias de Markov de ordem N-1.

  • Um grama (1 grama), unigrama, cadeia de Markov de ordem zero, não depende de nenhuma palavra anterior;
  • Gramática binária (2 gramas), bigrama, cadeia de Markov de primeira ordem, depende apenas da primeira palavra;
  • Trigrama (3 gramas), trigrama, cadeia de Markov de segunda ordem, depende apenas das 2 primeiras palavras;
  • ……

Preveja a probabilidade de uma palavra aparecer no tempo t até as primeiras t-1 palavras e use a estimativa de probabilidade máxima:

P ( w t ∣ w 1 , w 2 . . . w t − 1 ) = C ( w 1 , w 2 , . . . w t ) C ( w 1 , w 2 , . . . w t − 1 ) P(w_t | w_1,w_2...w_{t-1}) = \frac{C(w_1,w_2,...w_t)}{C(w_1,w_2,...w_{t-1})} P(wtw1,w2...wt1)=C(w1,w2,...wt1)C(w1,w2,...wt)

进一步地,一组词(也就是一个句子)出现的概率就是:

P ( w 1 , w 2 , . . . w t ) = P ( w t ∣ w 1 , w 2 , . . . w t − 1 ) ⋅ P ( w t − 1 ∣ w 1 , w 2 , . . . w t − 2 ) ⋅ . . . ⋅ P ( w 1 ) = ∏ i = 1 t − 1 P ( w i ∣ w 1 : i − 1 ) \begin{aligned} P(w_1,w_2,...w_t) &= P(w_t | w_1,w_2,...w_{t-1}) \cdot P(w_{t-1} | w_1,w_2,...w_{t-2}) \cdot ... \cdot P(w_1) \\ &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{1:i-1}) \end{aligned} P(w1,w2,...wt)=P(wtw1,w2,...wt1)P(wt1w1,w2,...wt - 2)...P ( w1)=eu = 1t - 1P ( weuw1 : i 1)

A fim de resolver o problema de calcular a probabilidade do início e fim de uma frase, introduzimos duas tags <BOS> e <EOS> para representar o início e o fim da frase respectivamente, então w 0 = w_0 =c0= <BOS>、wcomprimento + 1 = w_{comprimento + 1} =cl e n g t + 1 _= <EOS>, onde comprimento é o número de palavras.

Especificamente, por exemplo, para bigrama, o modelo é expresso da seguinte forma:

P ( w 1 , w 2 , . . . wt ) = ∏ i = 1 t − 1 P ( wi ∣ wi − 1 ) P ( wt ∣ wt − 1 ) = C ( wt − 1 , wt ) C ( wt − 1 ) \begin{aligned} P(w_1,w_2,...w_t) &= \displaystyle\prod_{i=1}^{t-1}P(w_i | w_{i-1}) \\ P( w_t | w_{t-1}) &= \frac{C(w_{t-1}, w_t)}{C(w_{t-1})} \end{alinhado}P ( w1,c2,... wt)P ( wtwt - 1)=eu = 1t - 1P ( weuweu - 1)=C ( wt - 1)C ( wt - 1,ct)

  • Se o número de ocorrências de uma palavra for 0, a multiplicação dessa string for 0, o que devo fazer?
  • Por ser baseado na suposição de Markov, o valor da janela fixa N terá um desempenho ruim em palavras de longa distância.
  • Se o valor de N for definido como grande para resolver dependências de palavras de longa distância, isso levará a uma grave dispersão de dados (muitas frequências zero) e a escala de parâmetros explodirá rapidamente (cálculo de tensor de alta dimensão).

Para o primeiro problema acima, introduzimos métodos como suavização/regressão/diferença para resolvê-lo, enquanto os dois últimos problemas são melhor resolvidos após o surgimento do modelo de rede neural.

1.2. Suavização/Desconto

Embora limitar o tamanho da janela n reduza a possibilidade da probabilidade da palavra ser 0, mas quando o n do n-gram for relativamente grande, haverá um problema de palavra fora do registro (Out Of Vocabulary, OOV). Por outro lado, os dados de treinamento podem não cobrir 100% das palavras que podem ser encontradas na prática. Portanto, para evitar a ocorrência de probabilidade 0, existe uma tecnologia de patch que faz a transição suave de zero para diferente de zero.

A técnica de suavização mais simples é o desconto. Esta é uma maneira muito fácil de pensar, que é poupar uma pequena parte da probabilidade geral de 100% e fornecer essas palavras de frequência zero (também costumam considerar palavras de baixa frequência juntas). Métodos comuns de suavização incluem: mais 1 suavização, mais suavização K, suavização Good-Turing, suavização Katz, etc.

1.2.1, Desconto Adicional / Suavização de Laplace (Desconto Adicional / Suavização de Laplace)

Adicione 1 para suavizar, ou seja, aumente diretamente o número de ocorrências de todas as palavras em 1, não apenas para palavras de frequência zero e palavras de baixa frequência. Se você continuar a usar o bigrama como exemplo, o modelo se tornará:

P ( wi ∣ wi − 1 ) = C ( wi − 1 , wi ) + 1 ∑ j = 1 n ( C ( wi − 1 , wj ) + 1 ) = C ( wi − 1 , wi ) + 1 C ( wi − 1 ) + ∣ V ∣ P(w_i | w_{i-1}) = \frac{C_(w_{i-1},w_i) + 1}{\displaystyle\sum_{j=1}^n(C_ (w_{i-1},w_j) + 1)} = \frac{C(w_{i-1}, w_i) + 1}{C(w_{i-1}) + |\mathbb{V}| }P ( weuweu - 1)=j=1n(C(wi1,wj)+1)C(wi1,wi)+1=C(wi1)+VC(wi1,wi)+1

其中 N N N 表示所有词的词频之和, ∣ V ∣ |\mathbb{V}| V 表示词汇表的大小。

如果当词汇表中的词,很多出现次数都很小,这样对每个词的词频都 +1,结果的偏差影响其实挺大的。换句话说,+1 对于低频词很多的场景,加的太多了,应该加一个更小的数( 1 < δ < 1)。所以有了下面的「δ 平滑」技术。

1.2.2、加 K 平滑 / δ 平滑(Add-K Discounting / Delta Smoothing)

把 +1 换成 δ,我们看下上面 bigram 模型应该变成上面样子:

P ( w i ∣ w i − 1 ) = C ( w i − 1 , w i ) + δ ∑ j = 1 n ( C ( w i − 1 , w j ) + δ ) = C ( w i − 1 , w i ) + δ C ( w i − 1 ) + δ ∣ V ∣ P(w_i | w{i-1}) = \frac{C_(w_{i-1},w_i) + \delta}{\displaystyle\sum_{j=1}^n(C_(w_{i-1},w_j) + \delta)} = \frac{C(w_{i-1}, w_i) + \delta}{C(w_{i-1}) + \delta|\mathbb{V}|} P(wiwi1)=j=1n(C(wi1,wj)+δ)C(wi1,wi)+δ=C(wi1)+δVC(wi1,wi)+δ

δ 是一个超参数,确定它的值需要用到困惑度(Perplexity,一般用缩写 PPL)。另外,有些文章里也会把这个方法叫做「加 K 平滑,Add-K Smoothing」。

1.2.3、困惑度(Perplexity)

对于指定的测试集,困惑度定义为测试集中每一个词概率的几何平均数的倒数,公式如下:

PPL ⁡ ( D t e s t ) = 1 P ( w 1 , w 2 . . . w n ) n \operatorname{PPL}(\mathbb{D}_{test}) = \frac{1}{\sqrt[n]{P(w_1,w_2...w_n)}} PPL(Dtest)=nP(w1,w2...wn) 1

P ( w 1 , w 2 , . . . w t ) = ∏ i = 1 t − 1 P ( w i ∣ w i − 1 ) P(w_1,w_2,...w_t) = \displaystyle\prod_{i=1}^{t-1}P(w_i|w_{i-1}) P(w1,w2,...wt)=i=1t1P(wiwi1) 带入上述公式,就得到了 PPL 的计算公式:

PPL ⁡ ( D t e s t ) = ( ∏ i = 1 n P ( w i ∣ w 1 : i − 1 ) ) − 1 n \operatorname{PPL}(\mathbb{D}_{test}) = (\displaystyle\prod_{i=1}^nP(w_i|w_{1:i-1}))^{-\frac{1}{n}} PPL(Dtest)=(i=1nP(wiw1:i1))n1

1.3、回退(Back-off)

在多元文法模型中,比如以 3-gram 为例,如果出现某些三元语法概率为零,则不使用零来表示概率,而回退到 2-gram,如下。

P ( wi ∣ wi − 2 wi − 1 ) = { P ( wi ∣ wi − 2 wi − 1 ) C ( wi − 2 wi − 1 wi ) > 0 P ( wi ∣ wi − 1 ) C ( wi − 2 wi − 1 wi ) = 0 e C ( wi − 1 wi ) > 0 P(w_i|w_{i-2}w_{i-1}) = \begin{cases} P(w_i|w_{i-2}w_ {i-1}) & C(w_{i-2}w_{i-1}w_i) > 0 \\ P(w_i|w_{i-1}) & C(w_{i-2}w_{i -1}w_i) = 0 \enspace e \enspace C(w_{i-1}w_i) > 0 \end{cases}P ( weuweu 2ceu - 1)={ P ( weuweu 2ceu - 1)P ( weuweu - 1)C ( weu 2ceu - 1ceu)>0C ( weu 2ceu - 1ceu)=0e dC ( weu - 1ceu)>0

1.4. Interpolação

Se o modelo N-gram usa o método fallback, ele considera apenas que quando a probabilidade n-gram é 0, o fallback é n-1 grama, então é natural perguntar: quando o n-gram não é zero, pode também ser ponderado de acordo com um determinado peso?Considere n-1 grama? Portanto, existe um método de interpolação. Tome 3 gramas como exemplo, levando em consideração 2 gramas e 1 grama:

P ( wi ∣ wi − 2 wi − 1 ) = λ 1 P ( wi ∣ wi − 2 wi − 1 ) + λ 2 P ( wi ∣ wi − 1 ) + λ 3 P ( wi ) P(w_i|w_{i -2}w_{i-1}) = \lambda_1 P(w_i|w_{i-2}w_{i-1}) + \lambda_2 P(w_i|w_{i-1}) + \lambda_3 P(w_i )P ( weuweu 2ceu - 1)=eu1P ( weuweu 2ceu - 1)+eu2P ( weuweu - 1)+eu3P ( weu)

Seção 2 Perceptron

Os problemas significativos do modelo N-gram foram mencionados na seção "Hipótese de Markov e o Modelo de Linguagem N-gram". Esses problemas são resolvidos basicamente no modelo de rede neural, e para entender o modelo de rede neural, devemos começar pelo perceptron (Perceptron). O modelo perceptron foi proposto em 1957, e o modelo multi-layer perceptron (MLP) foi proposto em 1959. MLP é algumas vezes chamada de ANN, ou seja, Rede Neural Artificial, a seguir vamos entender de forma simples e fazer alguns exercícios práticos.

2.1, Perceptron (Perceptron): uma rede neural feed-forward para tarefas de classificação binária

x é um vetor de entrada, w é um vetor de peso (um vetor de valores de peso atribuídos a cada valor no vetor de entrada). Para dar um exemplo de uma tarefa específica, por exemplo, se o produto interno desses dois vetores exceder um determinado valor, ele é julgado como 1, caso contrário, é 0. Na verdade, essa é uma tarefa de classificação. Então o valor de saída final pode ser expresso da seguinte forma:

y = { 1 ( ω ⋅ x ≥ 0 ) 0 ( ω ⋅ x < 0 ) y = \begin {casos} 1 & (\omega \cdot x \geq 0) \\ 0 & (\omega \cdot x \lt 0) \end{casos}y={ 10( ahx0 )( ahx<0 )

Este é um perceptron típico (Perceptron), que geralmente é usado para resolver problemas de classificação. Você também pode adicionar outro termo de viés (bias), da seguinte forma:

y = { 1 ( ω ⋅ x + b ≥ 0 ) 0 ( ω ⋅ x + b < 0 ) y = \begin{casos} 1 & (\omega \cdot x + b \geq 0) \\ 0 & (\ ômega \cdot x+b\lt0)\end{casos}y={ 10( ahx+b0 )( ahx+b<0 )

O perceptron é na verdade uma rede neural feed-forward que consiste em uma camada de entrada, uma camada de saída e nenhuma camada oculta. E a saída é uma função binária, que é usada para resolver problemas de classificação binária.

2.2, Regressão Linear (Regressão Linear): De perceptrons de valor discreto (resolução de problemas de classe) a regressão linear de valor contínuo (resolução de problemas de regressão)

Em geral, pensamos na saída de um perceptron como um valor discreto. De um modo geral, pensamos que o problema resolvido por valor discreto como saída é um problema de classificação; correspondentemente, o problema resolvido por valor contínuo é regressão (Regressão). Por exemplo, para o perceptron acima, se nós diretamente ω ⋅ x + b \omega \cdot x + bohx+b como valor de saída, torna-se um modelo de um problema de regressão linear.

Abaixo usamos o PyTorch para implementar um exemplo de código de regressão linear. Antes de mais nada, precisamos entender que existe uma função muito utilizada na biblioteca PyTorch:

nn.Linear(in_features, out_features)

Essa função inicializa automaticamente os pesos e desvios quando é criada e pode calcular uma transformação linear dos dados de entrada chamando sua forwardfunção . Especificamente, quando a entrada xé , forwarda função calcula y = ω ⋅ x + by = \omega \cdot x + by=ohx+b , dos quaisWWW ebbb são o peso e o viés nn.Linearda camada

Vamos pegar um exemplo de código completo:

import torch
import torch.nn as nn

# 定义模型
class LinearRegression(nn.Module):
    def __init__(self, input_size, output_size):
        super(LinearRegression, self).__init__()
        self.linear = nn.Linear(input_size, output_size)

    def forward(self, x):
        return self.linear(x)

# 初始化模型
model = LinearRegression(input_size=1, output_size=1)

# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 创建输入特征 X 和标签 y
X = torch.Tensor([[1], [2], [3], [4]])
y = torch.Tensor([[2], [4], [6], [8]])

# 训练模型
for epoch in range(100):
    # 前向传播,在本文 2.7 节有详细介绍
    predictions = model(X)
    loss = criterion(predictions, y)

    # 反向传播,在本文 2.7 节有详细介绍
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# 创建测试数据 X_test 和标签 y_test
X_test = torch.Tensor([[5], [6], [7], [8]])
y_test = torch.Tensor([[10], [12], [14], [16]])

# 测试模型
with torch.no_grad():
    predictions = model(X_test)
    loss = criterion(predictions, y_test)
    print(f'Test loss: {
      
      loss:.4f}')

O código acima primeiro cria uma classe de modelo de regressão LinearRegressionlinear , que tem uma forwardfunção de propagação direta, que na verdade calcula o valor de saída quando chamado y.

O programa principal primeiro cria uma instância de modelo de regressão linear e, em seguida, define um avaliador de função de perda para avaliar o efeito do modelo e usa o Stochastic Gradient Descent como otimizador.

Em seguida, crie um tensor de recurso de entrada e um tensor de rótulo. Use este conjunto de recursos e rótulos para treinamento. O processo de treinamento é Xcalcular predictions, e então calcular a perda para o avaliador yjunto com loss, e então realizar a retropropagação (apresentado na Seção 2.7 deste artigo). Observe as três linhas de código para retropropagação:

optimizer.zero_grad()
loss.backward()
optimizer.step()

Esse treinamento é realizado 100 vezes (cada vez que os parâmetros do modelo serão atualizados em uma caixa preta, um epoché um processo de treinamento, às vezes chamado iterationde ou step, otimize continuamente os parâmetros do modelo de acordo com losso treinamento .

Em seguida, criamos um conjunto de tensores de valor de recurso de teste X_teste tensores de rótulo de teste y_teste os usamos para testar o desempenho do modelo predictionse y_testpara o avaliador para obter loss. Neste exemplo obtemos o seguinte resultado:

Test loss: 0.0034

2.3, Regressão Logística (Logistic Regression): regressão linear sem restrições de intervalo, para regressão logística limitada a um intervalo (frequentemente usado em problemas de classificação)

Você pode ver o problema de regressão linear, o valor de saída não é limitado. Se limitado (limite) em um específico ( 0 , L ) (0, L)( 0 ,L ) , é chamada de regressão logística. Então, como você transforma uma regressão linear em uma regressão logística? Geralmente, é transformada pela seguinte fórmula:

y = L 1 + e − k ( z − z 0 ) y = \frac{L}{1 + e^{-k(z-z_0)}}y=1+ek ( z z0)L

Então o original z ∈ ( − ∞ , + ∞ ) z \in (-\infty, +\infty)z( ,+ ) é transformado emy ∈ ( 0 , L ) y \in (0, L)y( 0 ,L ) .

  • Função de ativação : Esta função que limita o valor de saída a uma faixa alvo é chamada de Função de Ativação .
  • A inclinação da função é dada por kkcontrole k , quanto maior, mais íngreme.
  • z = z 0 z = z_0z=z0时, y = L 2 y = \frac{L}{2}y=2L

Aqui está um exemplo de código para a biblioteca scikit-learn baseada em Python:

from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# 这是 scikit-learn 库里的一个简单的数据集
iris = load_iris()

# 把 iris 数据集拆分成训练集和测试集两部分
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.25, random_state=42)

# 用 scikit-learn 库创建一个逻辑回归模型的实例
lr = LogisticRegression()

# 用上边 split 出来的训练集数据,训练 lr 模型实例
lr.fit(X_train, y_train)

# 用训练过的模型,拿测试集的输入数据做测试
predictions = lr.predict(X_test)

# 用测试集的数据验证精确性
accuracy = lr.score(X_test, predictions)
print(accuracy)

2.4, Regressão Sigmóide (Sigmoid Regression): regressão logística normalizada, geralmente usada para tarefas de classificação binária

Quando L = 1, k = 1, z 0 = 0 L = 1, k = 1, z_0 = 0eu=1 ,k=1 ,z0=0 , a função de ativação neste momento éSigmoid SigmoidFunção S i g m o i d , também frequentemente expressa comoσ \sigmafunção σ , como segue:

y = 1 1 + e − zy = \frac{1}{1 + e^{-z}}y=1+ez1

A faixa de valores da regressão Sigmóide está exatamente entre (0, 1), por isso é sempre usada como uma função de ativação para normalização. E um modelo de regressão linear e, em seguida, normalizado com a função sigmóide, também é chamado de "regressão sigmóide". A palavra Sigmóide significa forma de S. Podemos ver sua imagem funcional da seguinte forma:

Por causa da normalização, o valor de saída também pode ser entendido como uma probabilidade. Por exemplo, se nos deparamos com um problema de classificação binária, então a saída corresponde à probabilidade de pertencer a esta categoria.

Tal modelo sigmoide pode ser expresso como:

y = Sigmóide ( W ⋅ x + b ) y = Sigmóide(W \cdot x + b)y=S i g m o i d ( Wx+b )

另外sigmoid sigmoidA derivada (isto é, gradiente) da função s i g m o i d é fácil de calcular: y ′ = y ⋅ ( 1 − y ) y' = y \cdot (1-y)y=y( 1y ) . Isso é muito conveniente para o "algoritmo de descida de gradiente" para otimizar os parâmetros do modelo de acordo com a perda. Regressão sigmóide, geralmente usada para tarefas de classificação binária. E quanto ao caso de mais do que binário? Isso leva à regressão Softmax abaixo.

2.5. Regressão Softmax (Regressão Softmax): de sigmóide para tarefas binárias a Softmax para tarefas de classificação multivariada

相对逻辑回归,Softmax 也称为多项逻辑回归。上面说 Sigmoid 一般用于解决二元分类问题,那么多元问题就要用 Softmax 回归了。我们来拿一个具体问题来解释,比如问题是对于任意输入的一个电商商品的图片,来判断这个图片所代表的的商品,属于哪个商品类目。假设我们一共有 100 个类目。那么一个图片比如说其所有像素值作为输入特征值,输出就是一个 100 维的向量 z z z,输出向量中的每个值 z i z_i zi 表示属于相对应类目的概率 y i y_i yi

y i = S o f t m a x ( z ) i = e z i e z 1 + e z 2 + . . . + e z 1 00 y_i = Softmax(z)_i = \frac{e^{z_i}}{e^{z_1} + e^{z_2} + ... + e^{z_100}} yi=Softmax(z)i=ez1+ez2+...+ez100ezi

那么最后得到的 y y y 向量中的每一项就对应这个输入 z z z 属于这 100 个类目的各自概率了。所以如果回归到一般问题,这个 Softmax 回归的模型就如下:

y = S oftmax ( W ⋅ x + b ) y = Softmax(W \cdot x + b)y=S o f t máx ( W _x+b )

Para o exemplo acima de fotos de produtos de comércio eletrônico, assumindo que o tamanho de cada foto é 512x512, a expansão desse modelo é a seguinte:

[ a 1 a 2 . . . y 100 ] = S oftmax ( [ w 1 , 1 , w 1 , 2 , . . . w 1 , 512 w 2 , 1 , w 2 , 2 , . . . w 2 , 512 . . . . . . . . . . . . w 100 , 1 , w 100 , 2 , . . . w 100 , 512 ] ⋅ [ x 1 x 2 . . . x 512 ] + [ b 1 b 2 . . . b 512 ] ) \begin{ bmatrix} y_1 \\ y_2 \\ ... \\ y_{100} \end{bmatrix} = Softmax(\begin{bmatrix} w_{1,1}, & w_{1,2}, & ... & w_{1, 512} \\ w_{2,1}, & w_{2,2}, & ... & w_{2, 512} \\ ... & ... & ... & .. . \\ w_{100,1}, & w_{100,2}, & ... & w_{100, 512} \end{bmatrix} \cdot \begin{bmatrix} x_1 \\ x_2 \\ ... \\ x_{512} \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \\ ... \\ b_{512} \end{bmatrix}) y1y2...y100 =S o f t x ( c1 , 1,c2 , 1,...c100 , 1,c1 , 2,c2 , 2,...c100 , 2,............c1 , 512c2 , 512...c100 , 512 x1x2...x512 + b1b2...b512 )

Isso para o vetor de entrada xxx realizaw ⋅ x + bw \cdot x + bcx+A operação b também é comumente referida como "mapeamento linear/mudança linear".

2.6, Perceptron de várias camadas (Perceptron de várias camadas)

Todas as tarefas que encontramos acima são resolvidas com modelos lineares. Às vezes, quando o problema se torna complicado, temos que introduzir um modelo não linear.

Aqui vamos introduzir uma nova função de ativação - R e LU ReLUR e LU (Unidade Linear Retificada) - uma função de ativação não linear, que é definida da seguinte forma:

R e LU ( z ) = max ( 0 , z ) ReLU(z) = max (0, z)RELU ( z ) _ _=x ( 0 ,z )

Por exemplo, o problema de classificação de dígitos manuscritos do conjunto de dados MNIST é uma tarefa típica de classificação não linear. Aqui está um exemplo de código:

import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms

# 定义多层感知器模型
class MLP(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

# 超参数
input_size = 784
hidden_size = 500
num_classes = 10
num_epochs = 5
batch_size = 100
learning_rate = 0.001

# 加载 MNIST 数据集
train_dataset = datasets.MNIST(root='../../data',
                               train=True,
                               transform=transforms.ToTensor(),
                               download=True)

test_dataset = datasets.MNIST(root='../../data',
                              train=False,
                              transform=transforms.ToTensor())

# 数据加载器
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=batch_size,
                                          shuffle=False)

model = MLP(input_size, hidden_size, num_classes)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

# 训练模型
for epoch in range(num_epochs):
    for images, labels in train_loader:
        # 前向传播
        outputs = model(images)
        loss = criterion(outputs, labels)

        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    # 输出训练损失
    print(f'Epoch {
      
      epoch + 1}, Training Loss: {
      
      loss.item():.4f}')

Neste código, podemos ver que a definição do modelo de MLP é:

nn.Linear(input_size, hidden_size)
nn.ReLU()
nn.Linear(hidden_size, num_classes)

Semelhante ao código de amostra do modelo anterior, o backpropagation, o avaliador de função de perda e o otimizador que apresentarei na Seção 2.7 deste artigo também são usados. Se expresso como uma fórmula, é a seguinte definição de modelo:

z = W 1 ⋅ x + b 1 h = R e LU ( z ) y = W 2 ⋅ h + b 2 \begin{aligned} &z = W_1 \cdot x + b_1 \\ &h = ReLU(z) \\ &y = W_2 \cdot h + b_2 \end{alinhado}z=C1x+b1h=RELU ( z ) _ _y=C2h+b2

Sabemos que o MLP é geralmente um modelo com os mesmos comprimentos de entrada e saída, mas em alguns casos também é possível construir um modelo MLP com comprimentos de entrada e saída diferentes. Por exemplo, depois de inserir um conjunto de sequências, a saída é um resultado de classificação discreta.

2.7. Descreva resumidamente como treinar um modelo: propagação direta e propagação reversa

Esta é uma questão muito importante. No entanto, o tempo para o Festival da Primavera é limitado, então esta parte só pode ser abreviada, focando mais no modelo de linguagem em si. Aqui está uma breve introdução, que pode ser complementada posteriormente.

  • O treinamento de uma rede neural inclui principalmente duas etapas: propagação direta e propagação reversa.
  • A propagação direta é inserir dados no modelo e obter resultados de saída com base em um conjunto de parâmetros determinados (como peso W em MLP, viés b, etc.). Calcule a função de perda com base nos resultados de saída para medir o desempenho do modelo sob os parâmetros atuais.
  • O método de retropropagação mais comumente usado é o método de gradiente descendente (outros métodos não são discutidos aqui), contando com a função de perda, usando os parâmetros como variáveis ​​para encontrar a derivada parcial (calcular o gradiente) e resolver a função de perda ao longo da direção do valor do gradiente descendente, o parâmetro neste momento pode substituir o parâmetro anterior. Este é um processo típico de treinamento de otimização de modelo.

Problema estendido - desaparecimento de gradiente, problema de explosão de gradiente: porque a derivada parcial da função de perda é calculada da camada de saída para a camada de entrada ao contrário com base na "regra da cadeia matemática". Matematicamente, este é um cálculo de multiplicação. Quanto maior o número, mais propenso a esse problema. Nesse processo de derivação, o gradiente pode ser zero, ou seja, o gradiente desaparece. Também é possível que o valor do gradiente seja particularmente grande.

Resolver o problema de desaparecimento e explosão de gradiente é outro tópico importante, e é difícil expandir as notas técnicas aqui devido a limitações de espaço. Métodos aproximados, como recorte de gradiente, o pré-treinamento camada por camada proposto por Hinton e, em seguida, o ajuste fino geral teoricamente também funcionam. O LSTM e o ResNet mencionados posteriormente neste artigo também podem resolver o problema, e também podemos aprender sobre diversas soluções do setor. , tem a oportunidade de trocar e aprender com os amigos.

2.8, um problema significativo de MLP, nos ajude a levar ao modelo CNN

Podemos ver que no MLP, não importa quantas camadas existam, o vetor de saída hn h_n de uma determinada camadahnPara cada valor em , o vetor de saída hn + 1 h_{n+1} será calculado na próxima camadahn + 1usado para cada valor de . Especificamente, se o valor de saída para uma determinada camada for o seguinte:

h n + 1 = S o f t m a x ( W n + 1 ⋅ h n + b n + 1 ) h_{n+1} = Softmax(W_{n+1} \cdot h_n + b_{n+1}) hn+1=Softmax(Wn+1hn+bn+1)

上一段话里所谓的「用到」,其实就是要针对 h n h_n hn 生成相应的特征值 W n + 1 W_{n+1} Wn+1 权重矩阵中的每个行列里的数值和 b n + 1 b_{n+1} bn+1 偏差向量里的每个值。如果用图画出来,就是:

可以看到,输入的所有元素都被连接,即被分配权重 w 和偏差项 b,所以这被称为一个「全连接层(Fully Connected Layer)」或者「稠密层(Dense Layer)」。但是对于一些任务这样做是很蠢的,会付出大量无效的计算。

因此我们需要 focus 在更少量计算成本的模型,于是有了卷积神经网络(CNN)。

第 3 节 · 卷积神经网络(CNN)

MLP 里每一层的每个元素,都要乘以一个独立参数的权重 W,再加上一个偏执 b,这样的神经网络层常被我们叫做「全连接层(Fully Connected Layer)或稠密层(Dence Layer)。但是这样有个显著问题:如果输入内容的局部重要信息只是发生轻微移动并没有丢失,在全连接层处理后,整个输出结果都会发生很大变化 —— 这不合理。

于是我们会想到,如果我们用一个小一些的全连接层,只对重要的局部输入进行处理呢?其实这个思路和 n-gram 是类似的,都是用一个窗口来扫描局部。卷积神经网络(Convolutional Neural Network,CNN)就是基于此诞生的。

  • 卷积核:卷积核是一个小的稠密层,用于提取局部特征,又称其为卷积核(kernel)/ 滤波器(filter)/ 感受野(receptive field / field of view)。
  • 池化层(Pooling,或称汇聚层):经过卷积核处理的结果,进一步聚合的过程。对于输入大小不一样的样本,池化后将有相同个数的特征输出。
  • 提取多个局部特征:一个卷积核只能提取单一类型的局部特征,需要提取多种局部特征则需要多个卷积核。有些文章里你看提到「多个模式」、「多个通道」,其实指的就是多个 kernel 识别多个特征。
  • 全连接分类层:多个卷积核得到的多个特征,需经过一个全连接的分类层用于最终决策。

这样做有几个特性:

  • 本地性(Locality):输出结果只由一个特定窗口大小区域内的数据决定。
  • 平移不变性(Translation Invariant):对同一个特征,扫描不同区域时只用一个 kernel 来计算。
  • 卷积层的参数规模,与输入输出数据大小无关。

CNN 主要的适用领域是计算机视觉。而在 NLP 中,文本数据的维度很高,并且语言的结构比图像更复杂。因此,CNN 一般不适用于处理 NLP 问题。

第 4 节 · 循环神经网络(RNN)

RNN(循环神经网络),这是一种强大的神经网络模型,能够预测序列数据,例如文本、语音和时间序列。我们将通过生动的代码示例和实际案例来演示如何使用 RNN,并在日常生活中真实地体验它的功能。您将学习到如何使用 RNN 解决各种机器学习问题,并动手尝试运用 RNN 解决实际问题。这篇文章将为您提供一个完整的 RNN 入门指南,并使您对 RNN 有更深入的了解。

RNN(Recurrent Neural Network)的 R 是 Recurrent 的意思,所以这是一个贷循环的神经网络。首先要明白一点,你并不需要搞懂 CNN 后才能学习 RNN 模型。你只要了解了 MLP 就可以学习 RNN 了。

4.1、经典结构的 RNN

上图这是一个经典结构的 RNN 示意图,Unfold 箭头右侧是展开示意。输入序列(这里用 x x x 表示)传递给隐藏层(hidden layer,这里用 h h h 表示),处理完生成输出序列(这里用 o o o 表示)。序列的下一个词输入时的、上一步隐藏层会一起影响这一步的输出。 U U U V V V W W W 都表示权重。在这个经典结构理,你可以看到非常重要的一点,就是输入序列长度与输出序列长度是相同的。

这种经典结构的应用场景,比如对一段普通话输入它的四川话版本,比如对视频的每一帧进行处理并输出,等等。

我们知道 RNN 是一个一个序列处理的,每个序列中的数据项都是有序的,所以对于计算一个序列内的所有数据项是无法并行的。但是计算不同序列时,不同序列各自的计算则是可以并行的。如果我们把上一个时刻 t 隐藏层输出的结果 h t − 1 h_{t-1} ht1Passe-o para uma função de ativação (por exemplo, use a tanhfunção ) e compare-o com a entrada xt x_{t} no momento atual txtJuntos, um tempo tt é gerado após o processamentosaídaht h_t de tht. Em seguida, passe a saída da camada oculta por regressão logística multinomial (Softmax) para gerar o valor de saída final yyy , podemos representar esse modelo da seguinte forma:

ht = tanh ( W xh ⋅ xt + bxh + W hh ⋅ ht − 1 + bhh ) yt = S oftmax ( Por que ⋅ ht + bhy ) \begin{aligned} &h_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{ alinhado}ht=Inglês ( W_ _x hxt+bx h+Chhht - 1+bhh )yt=S o f t máx ( W _oi _ht+bhy)

对应的示意图如下:

这种输入和输出数据项数一致的 RNN,一般叫做 N vs. N 的 RNN。如果我们用 PyTorch 来实现一个非常简单的经典 RNN 则如下:

import torch
import torch.nn as nn

# 创建一个 RNN 实例
# 第一个参数
rnn = nn.RNN(10, 20, 1, batch_first=True)  # 实例化一个单向单层RNN

# 输入是一个形状为 (5, 3, 10) 的张量
# 5 个输入数据项(也可以说是样本)
# 3 个数据项是一个序列,有 3 个 steps
# 每个 step 有 10 个特征
input = torch.randn(5, 3, 10)

# 隐藏层是一个 (1, 5, 20) 的张量
h0 = torch.randn(1, 5, 20)

# 调用 rnn 函数后,返回输出、最终的隐藏状态
output, hn = rnn(input, h0)

print(output)
print(hn)

我们来解读一下这段代码:

  • 这段代码实例化了一个带有 1 个隐藏层的 RNN 网络。
  • 它的输入是一个形状为 (5, 3, 10) 的张量,表示有 5 个样本,每个样本有 3 个时间步,每个时间步的特征维度是 10。
  • 初始隐藏状态是一个形状为 (1, 5, 20) 的张量。
  • 调用 rnn 函数后,会返回输出和最终的隐藏状态。
  • 输出的形状是 (5, 3, 20),表示有 5 个样本,每个样本有 3 个时间步,每个时间步的输出维度是 20。
  • 最终的隐藏状态的形状是 (1, 5, 20),表示最后的隐藏状态是 5

但是上面的代码示例,并没有自己编写一个具体的 RNN,而是用了默认的 PyTorch 的 RNN,那么下面我们就自己编写一个:

class MikeCaptainRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super().__init__()

        # 对于 RNN,输入维度就是序列数
        self.input_size = input_size

        # 隐藏层有多少个节点/神经元,经常将 hidden_size 设置为与序列长度相同
        self.hidden_size = hidden_size

        # 输入层到隐藏层的 W^{xh} 权重、bias^{xh} 偏置项
        self.weight_xh = torch.randn(self.hidden_size, self.input_size) * 0.01
        self.bias_xh = torch.randn(self.hidden_size)

        # 隐藏层到隐藏层的 W^{hh} 权重、bias^{hh} 偏置项
        self.weight_hh = torch.randn(self.hidden_size, self.hidden_size) * 0.01
        self.bias_hh = torch.randn(self.hidden_size)

    # 前向传播
    def forward(self, input, h0):

    	# 取出这个张量的形状
        N, L, input_size = input.shape

        # 初始化一个全零张量
        output = torch.zeros(N, L, self.hidden_size)

        # 处理每个时刻的输入特征
        for t in range(L):

        	# 获得当前时刻的输入特征,[N, input_size, 1]。unsqueeze(n),在第 n 维上增加一维
            x = input[:, t, :].unsqueeze(2)  
            w_xh_batch = self.weight_xh.unsqueeze(0).tile(N, 1, 1)  # [N, hidden_size, input_size]
            w_hh_batch = self.weight_hh.unsqueeze(0).tile(N, 1, 1)  # [N, hidden_size, hidden_size]

            # bmm 是矩阵乘法函数
            w_times_x = torch.bmm(w_xh_batch, x).squeeze(-1)  # [N, hidden_size]。squeeze(n),在第n维上减小一维
            w_times_h = torch.bmm(w_hh_batch, h0.unsqueeze(2)).squeeze(-1)  # [N, hidden_size]
            h0 = torch.tanh(w_times_x + self.bias_ih + w_times_h + self.bias_hh)
            output[:, t, :] = h0
        return output, h0.unsqueeze(0)

源码解读都在注释中。

4.2、N vs.1 的 RNN

上面那个图里,如果只保留最后一个输出,那就是一个 N vs. 1 的 RNN 了。这种的应用场景,比如说判断一个文本序列是英语还是德语,比如根据一个输入序列来判断是一个正向情绪内容还是负向或者中性,或者比如根据一段语音输入序列来判断是哪一首曲子(听歌识曲)。

ht = tanh ( W xh ⋅ xt + bxh + W hh ⋅ ht − 1 + bhh ) y = S oftmax ( Por que ⋅ hn + bhy ) \begin{aligned} &h_t = tanh({W^{xh}} \cdot x_t + {b^{xh}} + {W^{hh}} \cdot h_{t-1} + {b^{hh}}) \\ &y = Softmax({W^{hy}} \cdot h_n + {b^{hy}}) \end{alinhado}ht=Inglês ( W_ _x hxt+bx h+Chhht - 1+bhh )y=S o f t máx ( W _oi _hn+boi ) _

Ou seja, neste modelo, cada sequência só produz a saída hn h_n quando a camada oculta processa o último item de dadoshnSe representado por um diagrama esquemático, é a seguinte estrutura:

4.3, 1 vs. N RNN

Por outro lado, na figura acima, se apenas um x for mantido, então é um 1 vs. N RNN. A aplicação desse tipo de cena, como AI para criar música e extrair ou reconhecer algum conteúdo de texto por meio de uma imagem.

ht = { tanh ( W xh ⋅ x + bxh + 0 + bhh ) ( t = 1 ) tanh ( 0 + bxh + W hh ⋅ ht − 1 + bhh ) ( t > 1 ) yt = S oftmax ( Por que ⋅ ht + bhy ) \begin{aligned} &h_t = \begin{cases} tanh(W^{xh} \cdot x + b^{xh} + 0 + b^{hh}) & (t=1) \\ tanh( 0 + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) & (t>1) \end{cases} \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{alinhado}ht={ Inglês ( W_ _x hx+bx h+0+bhh )t inglês ( 0+bx h+Chhht - 1+bhh )( t=1 )( t>1 )yt=S o f t máx ( W _oi _ht+boi ) _

O diagrama esquemático é o seguinte:

Até agora, podemos ver que a camada oculta da RNN pode armazenar algum conteúdo relevante sobre os dados de entrada; portanto, a camada oculta da RNN é frequentemente chamada de unidade de memória.

4.4, LSTM (Long Short-Term Memory) rede de memória de longo e curto prazo

4.4.1 Como entender este Curto Prazo?

O modelo LSTM foi proposto no artigo de 1997 "Long Short-Term Memory". Vamos começar com a definição do modelo e entendê-lo com precisão:

ht = ht − 1 + tanh ( W xh ⋅ xt + bxh + W hh ⋅ ht − 1 + bhh ) yt = S oftmax ( Por que ⋅ ht + bhy ) \begin{aligned} &h_t = h_{t-1} + tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{alinhado}ht=ht - 1+Inglês ( W_ _x hxt+bx h+Chhht - 1+bhh )yt=S o f t máx ( W _oi _ht+boi ) _

上式中与经典结构的 RNN(输入与输出是 N vs. N)相比,唯一的区别是第一个式子中多了一个「 h t − 1 h_{t-1} ht1」。如果我们把第一个式子的 t a n h tanh tanh 部分记作 u t u_t ut

u t = t a n h ( W x h ⋅ x t + b x h + W h h ⋅ h t − 1 + b h h ) u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) ut=tanh(Wxhxt+bxh+Whhht1+bhh)

所以:

h t = h t − 1 + u t h_t = h_{t-1} + u_t ht=ht1+ut

那么可以展开出如下一组式子:

hk + 1 = hk + uk + 1 hk + 2 = hk + 1 + uk + 2 . . . . . . ht − 1 = ht − 2 + ut − 1 ht = ht − 1 + ut \begin{aligned} h_{k+1} &= h_k + u_{k+1} \\ h_{k+2} &= h_ {k+1} + u_{k+2} \\ &...... \\ h_{t-1} &= h_{t-2} + u_{t-1} \\ h_t &= h_ {t-1} + u_t \end{alinhado}hk + 1hk + 2ht - 1ht=hk+vocêk + 1=hk + 1+vocêk + 2......=ht - 2+vocêt - 1=ht - 1+vocêt

Se começarmos de hk + 1 h_{k+1}hk + 1para hn h_nhnSomando todas as expressões à esquerda e somando à direita, obtemos a seguinte expressão:

hk + 1 + . . . + ht − 1 + ht = hk + hk + 1 + . . . + ht − 2 + ht − 1 + uk + 1 + uk + 2 + . . . + ut − 1 + ut \begin{alinhado} &h_{k+1} + ... + h_{t-1} + h_t \\ = &h_k + h_{k+1} + ... + h_{t- 2} + h_{t-1} \\+ &u_{k+1} + u_{k+2} + ... + u_{t-1} + u_t \end{alinhado}=+hk + 1+...+ht - 1+hthk+hk + 1+...+ht - 2+ht - 1vocêk + 1+vocêk + 2+...+vocêt - 1+vocêt

Deduz-se então que:

ht = hk + uk + 1 + uk + 2 + . . . + ut − 1 + ut h_t = h_k + u_{k+1} + u_{k+2} + ... + u_{t-1} + u_tht=hk+vocêk + 1+vocêk + 2+...+vocêt - 1+vocêt

A partir daqui, podemos ver que a saída da camada oculta no tempo t está diretamente relacionada à saída no tempo k, e a correlação entre o tempo t e o tempo k é uk + 1 u_{k+ 1 }vocêk + 1ut u_tvocêtIndicado por adição. Ou seja, existe uma memória de curto prazo (Short Term) de tk.

4.4.2. Introduza o portão de esquecimento f, portão de entrada i, portão de saída o, célula de memória c

Se tivermos a fórmula ht = ht − 1 + ut h_t = h_{t-1} + u_tht=ht - 1+vocêtQue tal atribuir um peso aos dois termos à direita? É o resultado da camada oculta calculando o próprio item de dados anterior pelo item de dados anterior através da camada oculta. Os dois são considerados como um par de razões de peso, como segue:

ft = sigmoide ( W f , xh ⋅ xt + bf , xh + W f , hh ⋅ xt − 1 + bf , hh ) ht = ft ⊙ ht − 1 + ( 1 − ft ) ⊙ ut \begin{aligned} &f_t = sigmóide(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &h_t = f_t \odot h_{t-1} + (1 - f_t) \odot u_t \end{alinhado}ft=s i g m o i d ( Wf , x hxt+bf , x h+Cf , hhxt - 1+bf , hh )ht=ftht - 1+( 1ft)vocêt

em:

  • ⊙ \odot 是 Hardamard 乘积,即张量的对应元素相乘。
  • f t f_t ft 是「遗忘门(Forget Gate)」,该值很小时 t-1 时刻的权重就很小,也就是「此刻遗忘上一刻」。该值应根据 t 时刻的输入数据、t-1 时刻数据在隐藏层的输出计算,而且其每个元素必须是 (0, 1) 之间的值,所以可以用 sigmoid 函数来得到该值:

但这种方式,对于过去 h t − 1 h_{t-1} ht1 和当下 u t u_t ut 形成了互斥,只能此消彼长。但其实过去和当下可能都很重要,有可能都恨不重要,所以我们对过去继续采用 f t f_t ft 遗忘门,对当下采用 i t i_t it 输入门(Input Gate):

ft = sigmóide ( W f , xh ⋅ xt + bf , xh + W f , hh ⋅ xt − 1 + bf , hh ) it = sigmóide ( W i , xh ⋅ xt + bi , xh + W i , hh ⋅ ht − 1 + bi , hh ) ht = ft ⊙ ht − 1 + it ⊙ ut \begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f, hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoide(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh } \cdot h_{t-1} + b^{i,hh}) \\ &h_t = f_t \odot h_{t-1} + i_t \odot u_t \end{alinhado}ft=s i g m o i d ( Wf , x hxt+bf , x h+Cf , hhxt - 1+bf , hh )eut=s i g m o i d ( Weu , x hxt+beu , x h+Ci,hhht1+bi,hh)ht=ftht1+itut

其中:

  • f t f_t ft 类似地,定义输入门 i t i_t it ,但是注意 f t f_t ft h t − 1 h_{t-1} ht1 而非 x t − 1 x_{t-1} xt1 有关。

再引入一个输出门:

o t = s i g m o i d ( W o , x h ⋅ x t + b o , x h + W o , h h ⋅ x t − 1 + b o , h h ) o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh}) ot=sigmoid(Wo,xhxt+bo,xh+Wo,hhxt1+bo,hh)

再引入记忆细胞 c t c_t ct,它是原来 h t h_t ht 的变体,与 t-1 时刻的记忆细胞有遗忘关系(通过遗忘门),与当下时刻有输入门的关系:

c t = f t ⊙ c t − 1 + i t ⊙ u t c_t = f_t \odot c_{t-1} + i_t \odot u_t ct=ftct1+itut

那么此时 h t h_t ht,我们可以把 h t h_t ht 变成:

h t = o t ⊙ t a n h ( c t ) h_t = o_t \odot tanh(c_t) ht=ottanh(ct)

记忆细胞这个概念还有有一点点形象的,它存储了过去的一些信息。OK,到此我们整体的 LSTM 模型就变成了这个样子:

f t = s i g m o i d ( W f , x h ⋅ x t + b f , x h + W f , h h ⋅ x t − 1 + b f , h h ) i t = s i g m o i d ( W i , x h ⋅ x t + b i , x h + W i , h h ⋅ h t − 1 + b i , h h ) o t = s i g m o i d ( W o , x h ⋅ x t + b o , x h + W o , h h ⋅ x t − 1 + b o , h h ) u t = t a n h ( W x h ⋅ x t + b x h + W h h ⋅ h t − 1 + b h h ) c t = f t ⊙ c t − 1 + i t ⊙ u t h t = o t ⊙ t a n h ( c t ) y t = S o f t m a x ( W h y ⋅ h t + b h y ) \begin{aligned} &f_t = sigmoid(W^{f,xh} \cdot x_t + b^{f,xh} + W^{f,hh} \cdot x_{t-1} + b^{f,hh}) \\ &i_t = sigmoid(W^{i,xh} \cdot x_t + b^{i,xh} + W^{i,hh} \cdot h_{t-1} + b^{i,hh}) \\ &o_t = sigmoid(W^{o,xh} \cdot x_t + b^{o,xh} + W^{o,hh} \cdot x_{t-1} + b^{o,hh}) \\ &u_t = tanh(W^{xh} \cdot x_t + b^{xh} + W^{hh} \cdot h_{t-1} + b^{hh}) \\ &c_t = f_t \odot c_{t-1} + i_t \odot u_t \\ &h_t = o_t \odot tanh(c_t) \\ &y_t = Softmax(W^{hy} \cdot h_t + b^{hy}) \end{aligned} ft=sigmoid(Wf,xhxt+bf,xh+Wf,hhxt1+bf,hh)it=sigmoid(Wi,xhxt+bi,xh+Wi,hhht1+bi,hh)ot=sigmoid(Wo,xhxt+bo,xh+Wo,hhxt1+bo,hh)ut=tanh(Wxhxt+bxh+Whhht1+bhh)ct=ftct1+itutht=ottanh(ct)yt=Softmax(Whyht+bhy)

4.5、双向循环神经网络、双向 LSTM

双向循环神经网络很好理解,就是两个方向都有,例如下图:

在 PyTorch 中使用 nn.RNN 就有参数表示双向:

bidirectional – If True, becomes a bidirectional RNN. Default: False

bidirectional:默认设置为 False。若为 True,即为双向 RNN。

4.6、堆叠循环神经网络(Stacked RNN)、堆叠长短时记忆网络(Stacked LSTM)

在 PyTorch 中使用 nn.RNN 就有参数表示双向:

num_layers – Number of recurrent layers. E.g., setting num_layers=2 would mean stacking two RNNs together to form a stacked RNN, with the second RNN taking in outputs of the first RNN and computing the final results. Default: 1

num_layers:隐藏层层数,默认设置为 1 层。当 num_layers >= 2 时,就是一个 stacked RNN 了。

4.7、N vs. M 的 RNN

对于输入序列长度(长度 N)和输出序列长度(长度 M)不一样的 RNN 模型结构,也可以叫做 Encoder-Decoder 模型,也可以叫 Seq2Seq 模型。首先接收输入序列的 Encoder 先将输入序列转成一个隐藏态的上下文表示 C。C 可以只与最后一个隐藏层有关,甚至可以是最后一个隐藏层生成的隐藏态直接设置为 C,C 还可以与所有隐藏层有关。

有了这个 C 之后,再用 Decoder 进行解码,也就是从把 C 作为输入状态开始,生成输出序列。

具体地,可以如下表示:

C = E n c o d e r ( X ) Y = D e c o d e r ( C ) \begin{aligned} &C = Encoder(X) \\ &Y = Decoder(C) \\ \end{aligned} C=Encoder(X)Y=Decoder(C)

进一步展开:

e t = E n c o d e r L S T M / G R U ( x t , e t − 1 ) C = f 1 ( e n ) d t = f 2 ( d t − 1 , C ) y t = D e c o d e r L S T M / G R U ( y t − 1 , d t − 1 , C ) \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned} etCdtyt=EncoderLSTM/GRU(xt,et1)=f1(en)=f2(dt1,C)=DecoderLSTM/GRU(yt1,dt1,C)

这种的应用就非常广了,因为大多数时候输入序列与输出序列的长度都是不同的,比如最常见的应用「翻译」,从一个语言翻译成另一个语言;再比如 AI 的一个领域「语音识别」,将语音序列输入后生成所识别的文本内容;还有比如 ChatGPT 这种问答应用等等。

Seq2Seq 模型非常出色,一直到 2018 年之前 NLP 领域里该模型已成为主流。但是它有很显著的问题:

  • 当输入序列很长时,Encoder 生成的 Context 可能就会出现所捕捉的信息不充分的情况,导致 Decoder 最终的输出是不尽如人意的。具体地,毕竟还是 RNN 模型,其词间距过长时还是会有梯度消失问题,根本原因在于用到了「递归」。当递归作用在同一个 weight matrix 上时,使得如果这个矩阵满足条件的话,其最大的特征值要是小于 1 的话,就一定出现梯度消失问题。后来的 LSTM 和 GRU 也仅仅能缓解问题,并不能根本解决。
  • 并行效果差:每个时刻的结果依赖前一时刻。

第 5 节 · 为什么说 RNN 模型没有体现「注意力」?

Encoder-Decoder 的一个非常严重的问题,是依赖中间那个 context 向量,则无法处理特别长的输入序列 —— 记忆力不足,会忘事儿。而忘事儿的根本原因,是没有「注意力」。

对于一般的 RNN 模型,Encoder-Decoder 结构并没有体现「注意力」—— 这句话怎么理解?当输入序列经过 Encoder 生成的中间结果(上下文 C),被喂给 Decoder 时,这些中间结果对所生成序列里的哪个词,都没有区别(没有特别关照谁)。这相当于在说:输入序列里的每个词,对于生成任何一个输出的词的影响,是一样的,而不是输出某个词时是聚焦特定的一些输入词。这就是模型没有注意力机制。

人脑的注意力模型,其实是资源分配模型。NLP 领域的注意力模型,是在 2014 年被提出的,后来逐渐成为 NLP 领域的一个广泛应用的机制。可以应用的场景,比如对于一个电商平台中很常见的白底图,其边缘的白色区域都是无用的,那么就不应该被关注(关注权重为 0)。比如机器翻译中,翻译词都是对局部输入重点关注的。

所以 Attention 机制,就是在 Decoder 时,不是所有输出都依赖相同的「上下文 C t C_t Ct」,而是时刻 t 的输出,使用 C t C_t Ct,而这个 C t C_t Ct 来自对每个输入数据项根据「注意力」进行的加权。

第 6 节 · 基于 Attention 机制的 Encoder-Decoder 模型

2015 年 Dzmitry Bahdanau 等人在论文《Neural Machine Translation by Jointly Learning to Align and Translate》 中提出了「Attention」机制,下面请跟着船长,麦克船长会深入浅出地为你解释清楚。

下图中 e i e_i ei 表示编码器的隐藏层输出, d i d_i di 表示解码器的隐藏层输出

更进一步细化关于 C t C_t Ct 部分,船长在此引用《基于深度学习的道路短期交通状态时空序列预测》一书中的图:

这个图里的 h ~ i \widetilde{h}_i h i 与上一个图里的 d i d_i di 对应, h i h_i hi 与上一个图里的 e i e_i ei 对应。

针对时刻 t t t 要产出的输出,隐藏层每一个隐藏细胞都与 C t C_t Ct 有一个权重关系 α t , i \alpha_{t,i} αt,i 其中 1 ≤ i ≤ n 1\le i\le n 1in,这个权重值与「输入项经过编码器后隐藏层后的输出 e i ( 1 ≤ i ≤ n ) e_i(1\le i\le n) ei1in、解码器的前一时刻隐藏层输出 d t − 1 d_{t-1} dt1」两者有关:

s i , t = s c o r e ( e i , d t − 1 ) α i , t = e x p ( s i , t ) ∑ j = 1 n e x p ( s j , t ) \begin{aligned} &s_{i,t} = score(e_i,d_{t-1}) \\ &\alpha_{i,t} = \frac{exp(s_{i,t})}{\textstyle\sum_{j=1}^n exp(s_{j,t})} \end{aligned} si,t=score(ei,dt1)αi,t=j=1nexp(sj,t)exp(si,t)

常用的 s c o r e score score 函数有:

  • 点积(Dot Product)模型: s i , t = d t − 1 T ⋅ e i s_{i,t} = {d_{t-1}}^T \cdot e_i si,t=dt1Tei
  • 缩放点积(Scaled Dot-Product)模型: s i , t = d t − 1 T ⋅ e i d i m e n s i o n s   o f   d t − 1   o r   e i s_{i,t} = \frac{ {d_{t-1}}^T \cdot e_i}{\sqrt{\smash[b]{dimensions\:of\:d_{t-1}\:or\:e_i}}} si,t=dimensionsofdt1orei dt1Tei,可避免因为向量维度过大导致点积结果太大

然后上下文向量就表示成:

C t = ∑ i = 1 n α i , t e i \begin{aligned} &C_t = \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \end{aligned} Ct=i=1nαi,tei

还记得 RNN 那部分里船长讲到的 Encoder-Decoder 模型的公式表示吗?

e t = E n c o d e r L S T M / G R U ( x t , e t − 1 ) C = f 1 ( e n ) d t = f 2 ( d t − 1 , C ) y t = D e c o d e r L S T M / G R U ( y t − 1 , d t − 1 , C ) \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C &= f_1(e_n) \\ d_t &= f_2(d_{t-1}, C) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C) \end{aligned} etCdtyt=EncoderLSTM/GRU(xt,et1)=f1(en)=f2(dt1,C)=DecoderLSTM/GRU(yt1,dt1,C)

加入 Attention 机制的 Encoder-Decoder 模型如下。

e t = E n c o d e r L S T M / G R U ( x t , e t − 1 ) C t = f 1 ( e 1 , e 2 . . . e n , d t − 1 ) d t = f 2 ( d t − 1 , C t ) y t = D e c o d e r L S T M / G R U ( y t − 1 , d t − 1 , C t ) \begin{aligned} e_t &= Encoder_{LSTM/GRU}(x_t, e_{t-1}) \\ C_t &= f_1(e_1,e_2...e_n,d_{t-1}) \\ d_t &= f_2(d_{t-1}, C_t) \\ y_t &= Decoder_{LSTM/GRU}(y_{t-1}, d_{t-1}, C_t) \end{aligned} etCtdtyt=EncoderLSTM/GRU(xt,et1)=f1(e1,e2...en,dt1)=f2(dt1,Ct)=DecoderLSTM/GRU(yt1,dt1,Ct)

这种同时考虑 Encoder、Decoder 的 Attention,就叫做「Encoder-Decoder Attention」,也常被叫做「Vanilla Attention」。可以看到上面最核心的区别是第二个公式 C t C_t Ct。加入 Attention 后,对所有数据给予不同的注意力分布。具体地,比如我们用如下的函数来定义这个模型:

e = t a n h ( W x e ⋅ x + b x e ) s i , t = s c o r e ( e i , d t − 1 ) α i , t = e s i , t ∑ j = 1 n e s j , t C t = ∑ i = 1 n α i , t e i d t = t a n h ( W d d ⋅ d t − 1 + b d d + W y d ⋅ y t − 1 + b y d + W c d ⋅ C t + b c d ) y = S o f t m a x ( W d y ⋅ d + b d y ) \begin{aligned} e &= tanh(W^{xe} \cdot x + b^{xe}) \\ s_{i,t} &= score(e_i,d_{t-1}) \\ \alpha_{i,t} &= \frac{e^{s_{i,t}}}{\textstyle\sum_{j=1}^n e^{s_{j,t}}} \\ C_t &= \displaystyle\sum_{i=1}^n \alpha_{i,t} e_i \\ d_t &= tanh(W^{dd} \cdot d_{t-1} + b^{dd} + W^{yd} \cdot y_{t-1} + b^{yd} + W^{cd} \cdot C_t + b^{cd}) \\ y &= Softmax(W^{dy} \cdot d + b^{dy}) \end{aligned} esi,tαi,tCtdty=tanh(Wxex+bxe)=score(ei,dt1)=j=1nesj,tesi,t=i=1nαi,tei=tanh(Wdddt1+bdd+Wydyt1+byd+WcdCt+bcd)=Softmax(Wdyd+bdy)

到这里你能发现注意力机制的什么问题不?这个注意力机制忽略了位置信息。比如 Tigers love rabbits 和 Rabbits love tigers 会产生一样的注意力分数。

第二章 · Transformer 在 2017 年横空出世

麦克船长先通过一个动画来看下 Transformer 是举例示意,该图来自 Google 的博客文章 《Transformer: A Novel Neural Network Architecture for Language Understanding》

中文网络里找到的解释得比较好的 blogs、answers,几乎都指向了同一篇博客:Jay Alammar 的《The Illustrated Transformer》,所以建议读者搭配该篇文章阅读。

Transformer 模型中用到了自注意力(Self-Attention)、多头注意力(Multiple-Head Attention)、残差网络(ResNet)与捷径(Short-Cut)。下面我们先通过第 1 到第 4 小节把几个基本概念讲清楚,然后在第 5 小节讲解整体 Transformer 模型就会好理解很多了。最后第 6 小节我们来一段动手实践。

第 7 节 · 自注意力机制(Self-Attention)

自注意力是理解 Transformer 的关键,原作者在论文中限于篇幅,没有给出过多的解释。以下是我自己的理解,能够比较通透、符合常识地去理解 Transformer 中的一些神来之笔的概念。

7.1、一段自然语言内容,其自身就「暗含」很多内部关联信息

在加入了 Attention 的 Encoder-Decoder 模型中,对输出序列 Y 中的一个词的注意力来自于输入序列 X,那么如果 X 和 Y 相等呢?什么场景会有这个需求?因为我们认为一段文字里某些词就是由于另外某些词而决定的,可以粗暴地理解为「完形填空」的原理。那么这样一段文字,其实就存在其中每个词的自注意力,举个例子:

老王是我的主管,我很喜欢他的平易近人。

对这句话里的「他」,如果基于这句话计算自注意力的话,显然应该给予「老王」最多的注意力。受此启发,我们认为:

一段自然语言中,其实暗含了:为了得到关于某方面信息 Q,可以通过关注某些信息 K,进而得到某些信息(V)作为结果。

Q Q Q 就是 query 检索/查询, K K K V V V 分别是 key、value。所以类似于我们在图书检索系统里搜索「NLP书籍」(这是 Q Q Q),得到了一本叫《自然语言处理实战》的电子书,书名就是 key,这本电子书就是 value。只是对于自然语言的理解,我们认为任何一段内容里,都自身暗含了很多潜在 Q Q Q- K K K- V V V 的关联。这是整体受到信息检索领域里 query-key-value 的启发的。

基于这个启发,我们将自注意力的公式表示为:

Z = S e l f A t t e n t i o n ( X ) = A t t e n t i o n ( Q , K , V ) \begin{aligned} Z = SelfAttention(X) = Attention(Q,K,V) \end{aligned} Z=SelfAttention(X)=Attention(Q,K,V)

X X X 经过自注意力计算后,得到的「暗含」了大量原数据内部信息的 Z Z Z。然后我们拿着这个带有自注意力信息的 Z Z Z 进行后续的操作。这里要强调的是, Z Z Z 向量中的每个元素 z i z_i zi 都与 X 的所有元素有某种关联,而不是只与 x i x_i xi 有关联。

7.2、如何计算 Q、K、V

Q、K、V 全部来自输入 X 的线性变换:

Q = W Q ⋅ X K = W K ⋅ X V = W V ⋅ X \begin{aligned} Q &= W^Q \cdot X \\ K &= W^K \cdot X \\ V &= W^V \cdot X \end{aligned} QKV=WQX=WKX=WVX

W Q 、 W K 、 W V W^Q、W^K、W^V WQWKWV 以随机初始化开始,经过训练就会得到非常好的表现。对于 X X X 中的每一个词向量 x i x_i xi,经过这个变换后得到了:

q i = W Q ⋅ x i k i = W K ⋅ x i v i = W V ⋅ x i \begin{aligned} q_i &= W^Q \cdot x_i \\ k_i &= W^K \cdot x_i \\ v_i &= W^V \cdot x_i \end{aligned} qikivi=WQxi=WKxi=WVxi

7.3、注意力函数:如何通过 Q、V 得到 Z

基于上面的启发,我们认为 X X X 经过自注意力的挖掘后,得到了:

  • 暗含信息 1:一组 query 与一组 key 之间的关联,记作 qk(想一下信息检索系统要用 query 先招到 key)
  • 暗含信息 2:一组 value
  • 暗含信息 3:qk 与 value 的某种关联

这三组信息,分别如何表示呢?这里又需要一些启发了,因为计算机科学其实是在「模拟还原」现实世界,在 AI 的领域目前的研究方向就是模拟还原人脑的思考。所以这种「模拟还原」都是寻找某一种近似方法,因此不能按照数学、物理的逻辑推理来理解,而应该按照「工程」或者「计算科学」来理解,想想我们大学时学的「计算方法」这门课,因此常需要一些启发来找到某种「表示」。

这里 Transformer 的作者,认为 Q Q Q K K K 两个向量之间的关联,是我们在用 Q Q Q 找其在 K K K 上的投影,如果 Q Q Q K K K 是单位长度的向量,那么这个投影其实可以理解为找「 Q Q Q K K K 向量之间的相似度」:

  • 如果 Q Q Q K K K 垂直,那么两个向量正交,其点积(Dot Product)为 0;
  • 如果 Q Q Q K K K 平行,那么两个向量点积为两者模积 ∥ Q ∥ ∥ K ∥ \|Q\|\|K\| Q∥∥K
  • 如果 Q Q Q K K K 呈某个夹角,则点积就是 Q Q Q K K K 上的投影的模。

因此「暗含信息 1」就可以用「 Q ⋅ K Q\cdot K QK」再经过 Softmax 归一化来表示。这个表示,是一个所有元素都是 0~1 的矩阵,可以理解成对应注意力机制里的「注意力分数」,也就是一个「注意力分数矩阵(Attention Score Matrix)」。

而「暗含信息 2」则是输入 X X X 经过的线性变换后的特征,看做 X X X 的另一种表示。然后我们用这个「注意力分数矩阵」来加持一下 V V V,这个点积过程就表示了「暗含信息 3」了。所以我们有了如下公式:

Z = A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q ⋅ K T ) ⋅ V \begin{aligned} Z = Attention(Q,K,V) = Softmax(Q \cdot K^T) \cdot V \end{aligned} Z=Attention(Q,K,V)=Softmax(QKT)V

其实到这里,这个注意力函数已经可以用了。有时候,为了避免因为向量维度过大,导致 Q ⋅ K T Q \cdot K^T QKT 点积结果过大,我们再加一步处理:

Z = A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q ⋅ K T d k ) ⋅ V \begin{aligned} Z = Attention(Q,K,V) = Softmax(\frac{Q \cdot K^T}{\sqrt{\smash[b]{d_k}}}) \cdot V \end{aligned} Z=Attention(Q,K,V)=Softmax(dk QKT)V

这里 d k d_k dk 是 K 矩阵中向量 k i k_i ki 的维度。这一步修正还有进一步的解释,即如果经过 Softmax 归一化后模型稳定性存在问题。怎么理解?如果假设 Q Q Q K K K 中的每个向量的每一维数据都具有零均值、单位方差,这样输入数据是具有稳定性的,那么如何让「暗含信息 1」计算后仍然具有稳定性呢?即运算结果依然保持零均值、单位方差,就是除以「 d k \sqrt{\smash[b]{d_k}} dk 」。

7.4、其他注意力函数

为了提醒大家这种暗含信息的表示,都只是计算方法上的一种选择,好坏全靠结果评定,所以包括上面的在内,常见的注意力函数有(甚至你也可以自己定义):

Z = A t t e n t i o n ( Q , K , V ) = { = S o f t m a x ( Q T K ) V = S o f t m a x ( Q K T d k ) V = S o f t m a x ( ω T t a n h ( W [ q ; k ] ) ) V = S o f t m a x ( Q T W K ) V = c o s i n e [ Q T K ] V Z = Attention(Q,K,V) = \begin{cases} \begin{aligned} &= Softmax(Q^T K) V \\ &= Softmax(\frac{Q K^T}{\sqrt{\smash[b]{d_k}}}) V \\ &= Softmax(\omega^T tanh(W[q;k])) V \\ &= Softmax(Q^T W K) V \\ &= cosine[Q^T K] V \end{aligned} \end{cases} Z=Attention(Q,K,V)= =Softmax(QTK)V=Softmax(dk QKT)V=Softmax(ωTtanh(W[q;k]))V=Softmax(QTWK)V=cosine[QTK]V

到这里,我们就从原始的输入 X X X 得到了一个包含自注意力信息的 Z Z Z 了,后续就可以用 Z Z Z 了。

第 8 节 · 多头注意力

到这里我们理解了「自注意力」,而 Transformer 这篇论文通过添加「多头」注意力的机制进一步提升了注意力层。我们先看下它是什么,然后看下它的优点。从本小节开始,本文大量插图引用自《The Illustrated Transformer》,作者 Jay Alammar 写出一篇非常深入浅出的图解文章,被大量引用,非常出色,再次建议大家去阅读。

Transformer 中用了 8 个头,也就是 8 组不同的 Q Q Q- K K K- V V V

Q 0 = W 0 Q ⋅ X ; K 0 = W 0 K ⋅ X ; V 0 = W 0 V ⋅ X Q 1 = W 1 Q ⋅ X ; K 1 = W 0 K ⋅ X ; V 1 = W 1 V ⋅ X . . . . Q 7 = W 7 Q ⋅ X ; K 7 = W 0 K ⋅ X ; V 7 = W 7 V ⋅ X \begin{aligned} Q_0 = W_0^Q \cdot X ;\enspace K_0 = &W_0^K \cdot X ;\enspace V_0 = W_0^V \cdot X \\ Q_1 = W_1^Q \cdot X ;\enspace K_1 = &W_0^K \cdot X ;\enspace V_1 = W_1^V \cdot X \\ &.... \\ Q_7 = W_7^Q \cdot X ;\enspace K_7 = &W_0^K \cdot X ;\enspace V_7 = W_7^V \cdot X \end{aligned} Q0=W0QX;K0=Q1=W1QX;K1=Q7=W7QX;K7=W0KX;V0=W0VXW0KX;V1=W1VX....W0KX;V7=W7VX

这样我们就能得到 8 个 Z Z Z

Z 0 = A t t e n t i o n ( Q 0 , K 0 , V 0 ) = S o f t m a x ( Q 0 ⋅ K 0 T d k ) ⋅ V 0 Z 1 = A t t e n t i o n ( Q 1 , K 1 , V 1 ) = S o f t m a x ( Q 1 ⋅ K 1 T d k ) ⋅ V 1 . . . Z 7 = A t t e n t i o n ( Q 7 , K 7 , V 7 ) = S o f t m a x ( Q 7 ⋅ K 7 T d k ) ⋅ V 7 \begin{aligned} &Z_0 = Attention(Q_0,K_0,V_0) = Softmax(\frac{Q_0 \cdot K_0^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_0 \\ &Z_1 = Attention(Q_1,K_1,V_1) = Softmax(\frac{Q_1 \cdot K_1^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_1 \\ &... \\ &Z_7 = Attention(Q_7,K_7,V_7) = Softmax(\frac{Q_7 \cdot K_7^T}{\sqrt{\smash[b]{d_k}}}) \cdot V_7 \\ \end{aligned} Z0=Attention(Q0,K0,V0)=Softmax(dk Q0K0T)V0Z1=Attention(Q1,K1,V1)=Softmax(dk Q1K1T)V1...Z7=Attention(Q7,K7,V7)=Softmax(dk Q7K7T)V7

然后我们把 Z 0 Z_0 Z0 Z 7 Z_7 Z7 沿着行数不变的方向全部连接起来,如下图所示:

我们再训练一个权重矩阵 W O W^O WO,然后用上面拼接的 Z 0 − 7 Z_{0-7} Z07 乘以这个权重矩阵:

于是我们会得到一个 Z 矩阵:

到这里就是多头注意力机制的全部内容,与单头注意力相比,都是为了得到一个 Z Z Z 矩阵,但是多头用了多组 Q Q Q- K K K- V V V,然后经过拼接、乘以权重矩阵得到最后的 Z Z Z。我们总览一下整个过程:

通过多头注意力,每个头都会关注到不同的信息,可以如下类似表示:

这通过两种方式提高了注意力层的性能:

  • 多头注意力机制,扩展了模型关注不同位置的能力。 Z Z Z 矩阵中的每个向量 z i z_i zi 包含了与 X X X 中所有向量 x i x_i xi 有关的一点编码信息。反过来说,不要认为 z i z_i zi 只与 x i x_i xi 有关。
  • 多头注意力机制,为注意力层提供了多个「表示子空间 Q Q Q- K K K- V V V」,以及 Z Z Z。这样一个输入矩阵 X X X,就会被表示成 8 种不同的矩阵 Z Z Z,都包含了原始数据信息的某种解读暗含其中。

第 9 节 · 退化现象、残差网络与 Short-Cut

9.1、退化现象

对于一个 56 层的神经网路,我们很自然地会觉得应该比 20 层的神经网络的效果要好,比如说从误差率(error)的量化角度看。但是华人学者何凯明等人的论文《Deep Residual Learning for Image Recognition》中给我们呈现了相反的结果,而这个问题的原因并不是因为层数多带来的梯度爆炸/梯度消失(毕竟已经用了归一化解决了这个问题),而是因为一种反常的现象,这种现象我们称之为「退化现象」。何凯明等人认为这是因为存在「难以优化好的网络层」。

9.2、恒等映射

如果这 36 层还帮了倒忙,那还不如没有,是不是?所以这多出来的 36 个网络层,如果对于提升性能(例如误差率)毫无影响,甚至更进一步,这 36 层前的输入数据,和经过这 36 层后的输出数据,完全相同,那么如果将这 36 层抽象成一个函数 f 36 f_{36} f36,这就是一个恒等映射的函数:

f 36 ( x ) = x f_{36}(x) = x f36(x)=x

回到实际应用中。如果我们对于一个神经网络中的连续 N 层是提升性能,还是降低性能,是未知的,那么则可以建立一个跳过这些层的连接,实现:

如果这 N 层可以提升性能,则采用这 N 层;否则就跳过。

这就像给了这 N 层神经网络一个试错的空间,待我们确认它们的性能后再决定是否采用它们。同时也可以理解成,这些层可以去单独优化,如果性能提升,则不被跳过。

9.3、残差网络(Residual Network)与捷径(Short-Cut)

如果前面 20 层已经可以实现 99% 的准确率,那么引入了这 36 层能否再提升「残差剩余那 1%」的准确率从而达到 100% 呢?所以这 36 层的网络,就被称为「残差网络(Residual Network,常简称为 ResNet)」,这个叫法非常形象。

而那个可以跳过 N 层残差网络的捷径,则常被称为 Short-Cut,也会被叫做跳跃链接(Skip Conntection),这就解决了上述深度学习中的「退化现象」。

第 10 节 · Transformer 的位置编码(Positional Embedding)

还记得我在第二部分最后提到的吗:

这个注意力机制忽略了位置信息。比如 Tigers love rabbits 和 Rabbits love tigers 会产生一样的注意力分数。

10.1、Transformer 论文中的三角式位置编码(Sinusoidal Positional Encoding)

现在我们来解决这个问题,为每一个输入向量 x i x_i xi 生成一个位置编码向量 t i t_i ti,这个位置编码向量的维度,与输入向量(词的嵌入式向量表示)的维度是相同的:

Transformer 论文中给出了如下的公式,来计算位置编码向量的每一位的值:

P p o s , 2 i = s i n ( p o s 1000 0 2 i d m o d e l ) P p o s , 2 i + 1 = c o s ( p o s 1000 0 2 i d m o d e l ) \begin{aligned} P_{pos,2i} &= sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \\ P_{pos,2i+1} &= cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}}) \end{aligned} Ppos,2iPpos,2i+1=sin(10000dmodel2ipos)=cos(10000dmodel2ipos)

这样对于一个 embedding,如果它在输入内容中的位置是 pos,那么其编码向量就表示为:

[ P p o s , 0 , P p o s , 1 , . . . , P p o s , d x − 1 ] \begin{aligned} [P_{pos,0}, P_{pos,1}, ... , P_{pos,d_x-1}] \end{aligned} [Ppos,0,Ppos,1,...,Ppos,dx1]

延展开的话,位置编码其实还分为绝对位置编码(Absolute Positional Encoding)、相对位置编码(Relative Positional Encoding)。前者是专门生成位置编码,并想办法融入到输入中,我们上面看到的就是一种。后者是微调 Attention 结构,使得它可以分辨不同位置的数据。另外其实还有一些无法分类到这两种的位置编码方法。

10.2、绝对位置编码

绝对位置编码,如上面提到的,就是定义一个位置编码向量 t i t_i ti,通过 x i + t i x_i + t_i xi+ti 就得到了一个含有位置信息的向量。

  • 习得式位置编码(Learned Positional Encoding):将位置编码当做训练参数,生成一个「最大长度 x 编码维度」的位置编码矩阵,随着训练进行更新。目前 Google BERT、OpenAI GPT 模型都是用的这种位置编码。缺点是「外推性」差,如果文本长度超过之前训练时用的「最大长度」则无法处理。目前有一些给出优化方案的论文,比如「层次分解位置编码」。
  • 三角式位置编码(Sinusoidal Positional Encodign):上面提过了。
  • 循环式位置编码(Recurrent Positional Encoding):通过一个 RNN 再接一个 Transformer,那么 RNN 暗含的「顺序」就导致不再需要额外编码了。但这样牺牲了并行性,毕竟 RNN 的两大缺点之一就有这个。
  • 相乘式位置编码(Product Positional Encoding):用「 x i ⊙ t i x_i \odot t_i xiti」代替「 x i + t i x_i + t_i xi+ti」。

10.3、相对位置编码和其他位置编码

最早来自于 Google 的论文《Self-Attention with Relative Position Representations》相对位置编码,考虑的是当前 position 与被 attention 的 position 之前的相对位置。

  • 常见相对位置编码:经典式、XLNET 式、T5 式、DeBERTa 式等。
  • 其他位置编码:CNN 式、复数式、融合式等。

到此我们都是在讲 Encoder,目前我们知道一个 Encoder 可以用如下的示意图表示:

第 11 节 · Transformer 的编码器 Encoder 和解码器 Decoder

11.1、Encoder 和 Decoder 的图示结构

  • 第一层是多头注意力层(Multi-Head Attention Layer)。
  • 第二层是经过一个前馈神经网络(Feed Forward Neural Network,简称 FFNN)。
  • 这两层,每一层都有「Add & Normalization」和 ResNet。

  • 解码器有两个多头注意力层。第一个多头注意力层是 Masked Multi-Head Attention 层,即在自注意力计算的过程中只有前面位置上的内容。第二个多头注意力层买有被 Masked,是个正常多头注意力层。
  • 可以看出来,第一个注意力层是一个自注意力层(Self Attention Layer),第二个是 Encoder-Decoder Attention 层(它的 K K K V V V 来自 Encoder, Q Q Q 来自自注意力层),有些文章里会用这个角度来指代。
  • FNN、Add & Norm、ResNet 都与 Encoder 类似。

11.2、Decoder 的第一个输出结果

产出第一个最终输出结果的过程:

  • 不需要经过 Masked Multi-Head Attention Layer(自注意力层)。
  • 只经过 Encoder-Decoder Attention Layer。

这样我们就像前面的 Encoder-Decoder Attention 模型一样,得到第一个输出。但是最终的输出结果,还会经过一层「Linear + Softmax」。

11.3、Decoder 后续的所有输出

从产出第二个输出结果开始:

  • Decoder 的自注意力层,会用到前面的输出结果。
  • 可以看到,这是一个串行过程。

11.4、Decoder 之后的 Linear 和 Softmax

经过所有 Decoder 之后,我们得到了一大堆浮点数的结果。最后的 Linear & Softmax 就是来解决「怎么把它变成文本」的问题的。

  • Linear 是一个全连接神经网络,把 Decoders 输出的结果投影到一个超大的向量上,我们称之为 logits 向量。
  • 如果我们的输出词汇表有 1 万个词,那么 logits 向量的每一个维度就有 1 万个单元,每个单元都对应输出词汇表的一个词的概率。
  • Softmax 将 logits 向量中的每一个维度都做归一化,这样每个维度都能从 1 万个单元对应的词概率中选出最大的,对应的词汇表里的词,就是输出词。最终得到一个输出字符串。

第 12 节 · Transformer 模型整体

最后我们再来整体看一下 Transformer:

  • 首先输入数据生成词的嵌入式向量表示(Embedding),生成位置编码(Positional Encoding,简称 PE)。
  • 进入 Encoders 部分。先进入多头注意力层(Multi-Head Attention),是自注意力处理,然后进入全连接层(又叫前馈神经网络层),每层都有 ResNet、Add & Norm。
  • 每一个 Encoder 的输入,都来自前一个 Encoder 的输出,但是第一个 Encoder 的输入就是 Embedding + PE。
  • 进入 Decoders 部分。先进入第一个多头注意力层(是 Masked 自注意力层),再进入第二个多头注意力层(是 Encoder-Decoder 注意力层),每层都有 ResNet、Add & Norm。
  • 每一个 Decoder 都有两部分输入。
  • Decoder 的第一层(Maksed 多头自注意力层)的输入,都来自前一个 Decoder 的输出,但是第一个 Decoder 是不经过第一层的(因为经过算出来也是 0)。
  • Decoder 的第二层(Encoder-Decoder 注意力层)的输入,Q 都来自该 Decoder 的第一层,且每个 Decoder 的这一层的 K、V 都是一样的,均来自最后一个 Encoder。
  • 最后经过 Linear、Softmax 归一化。

第 13 节 · Transformer 的性能

Google 在其博客于 2017.08.31 发布如下测试数据:

第三章 · 一个基于 TensorFlow 架构的 Transformer 实现

我们来看看一个简单的 Transformer 模型,就是比较早出现的 Kyubyong 实现的 Transformer 模型:GitHub - Kyubyong Transformer - tf1.2 legacy

第 14 节 · 先训练和测试一下 Kyubyong Transformer

下载一个「德语-英语翻译」的数据集:https://drive.google.com/uc?id=1l5y6Giag9aRPwGtuZHswh3w5v3qEz8D8

de-en 下面的 tgz 解压后放在 corpora/ 目录下。如果需要先修改超参数,需要修改 hyperparams.py。然后运行如下命令,生成词汇文件(vocabulary files),默认到 preprocessed 目录下:

mikecaptain@local $ python prepro.py

然后开始训练:

mikecaptain@local $ python train.py

也可以跳过训练,直接下载预训练过的文件,是一个 logdir/ 目录,把它放到项目根目录下。然后可以对训练出来的结果,运行评价程序啦:

mikecaptain@local $ python eval.py

会生成「德语-英语」测试结果文件在 results/ 目录下,内容如下:

- source: Sie war eine jährige Frau namens Alex
- expected: She was a yearold woman named Alex
- got: She was a <UNK> of vote called <UNK>

- source: Und als ich das hörte war ich erleichtert
- expected: Now when I heard this I was so relieved
- got: And when I was I <UNK> 's

- source: Meine Kommilitonin bekam nämlich einen Brandstifter als ersten Patienten
- expected: My classmate got an arsonist for her first client
- got: Because my first eye was a first show

- source: Das kriege ich hin dachte ich mir
- expected: This I thought I could handle
- got: I would give it to me a day

- source: Aber ich habe es nicht hingekriegt
- expected: But I didn't handle it
- got: But I didn't <UNK> <UNK>

- source: Ich hielt dagegen
- expected: I pushed back
- got: I <UNK>

...

Bleu Score = 6.598452846670836

评估结果文件的最后一行是 Bleu Score:

  • 这是用来评估机器翻译质量的一种度量方式。它是由几个不同的 BLEU 分数组成的,每个 BLEU 分数都表示翻译结果中与参考翻译的重叠程度。
  • 一个常用的 BLEU 分数是 BLEU-4,它计算翻译结果中与参考翻译的 N 元文法语言模型 n-gram(n 为 4)的重叠程度。分数越高表示翻译结果越接近参考翻译。

第 15 节 · Kyubyong Transformer 源码分析

  • hparams.py:超参数都在这里,仅 30 行。将在下面 2.1 部分解读。
  • data_load.py:装载、批处理数据的相关函数,代码仅 92 行。主要在下面 2.2 部分解读。
  • prepro.py:为 source 和 target 创建词汇文件(vocabulary file),代码仅 39 行。下面 2.3 部分会为大家解读。
  • train.py:代码仅 184 行。在下面 2.4 部分解读。
  • modules.py:Encoding / Decoding 网络的构建模块,代码仅 329 行。与 modules.py 一起会在 2.4 部分解读。
  • eval.py:评估效果,代码仅 82 行。将在 2.5 部分解读

总计 700 多行代码。

15.1、超参数

hyperparams.py 文件中定义了 Hyperparams 超参数类,其中包含的参数我们逐一来解释一下:

  • source_train:训练数据集的源输入文件,默认是 'corpora/train.tags.de-en.de'
  • target_train:训练数据集的目标输出文件,默认是 'corpora/train.tags.de-en.en'
  • source_test:测试数据集的源输入文件,默认是 'corpora/IWSLT16.TED.tst2014.de-en.de.xml'
  • target_test:测试数据集的目标输出文件,默认是 'corpora/IWSLT16.TED.tst2014.de-en.en.xml'
  • batch_size:设置每批数据的大小。
  • lr:设置学习率 learning rate。
  • logdir:设置日志文件保存的目录。
  • maxlen
  • min_cnt
  • hidden_units:设置编码器和解码器中隐藏层单元的数量。
  • num_blocks:编码器(encoder block)、解码器(decoder block)的数量
  • num_epochs:训练过程中迭代的次数。
  • num_heads:还记得上面文章里我们提到的 Transformer 中用到了多头注意力吧,这里就是多头注意力的头数。
  • droupout_rate:设置 dropout 层的 dropout rate,具体 dropout 请看 2.4.1 部分。
  • sinusoid:设置为 True 时表示使用正弦函数计算位置编码,否则为 False 时表示直接用 position 做位置编码。

15.2、预处理

文件 prepro.py 实现了预处理的过程,根据 hp.source_trainhp.target_train 分别创建 "de.vocab.tsv""en.vocab.tsv" 两个词汇表。

def make_vocab(fpath, fname):

    # 使用 codecs.open 函数读取指定文件路径(fpath)的文本内容,并将其存储在 text 变量中
    text = codecs.open(fpath, 'r', 'utf-8').read()

    # 将 text 中的非字母和空格的字符去掉
    text = regex.sub("[^\s\p{Latin}']", "", text)

    # 将 text 中的文本按照空格分割,并将每个单词存储在 words 变量中
    words = text.split()

    # words 中每个单词的词频
    word2cnt = Counter(words)

    # 检查是否存在 preprocessed 文件夹,如果不存在就创建
    if not os.path.exists('preprocessed'): os.mkdir('preprocessed')
    with codecs.open('preprocessed/{}'.format(fname), 'w', 'utf-8') as fout:

    	# 按出现次数从多到少的顺序写入每个单词和它的出现次数
    	# 在文件最前面写入四个特殊字符 <PAD>, <UNK>, <S>, </S> 分别用于填充,未知单词,句子开始和句子结束
        fout.write("{}\t1000000000\n{}\t1000000000\n{}\t1000000000\n{}\t1000000000\n".format("<PAD>", "<UNK>", "<S>", "</S>"))
        for word, cnt in word2cnt.most_common(len(word2cnt)):
            fout.write(u"{}\t{}\n".format(word, cnt))

if __name__ == '__main__':
    make_vocab(hp.source_train, "de.vocab.tsv")
    make_vocab(hp.target_train, "en.vocab.tsv")
    print("Done")
  • 在主函数中调用 make_vocab 函数,在目录 preprocessed 生成 de.vocab.tsven.vocab.tsv 两个词汇表文件。
  • 在函数 make_vocab 中,先使用 codecs.open 函数读取指定文件路径 fpath 的文本内容,并将其存储在 text 变量中,再使用正则表达式 regextext 中的非字母和空格的字符去掉,接着将 text 中的文本按照空格分割,并将每个单词存储在 words 变量中。
  • 接下来,使用 Counter 函数统计 words 中每个单词的出现次数,并将统计结果存储在 word2cnt 变量中。
  • 最后所有词与词频,存储在 de.vocab.tsven.vocab.tsv 两个文件中。

15.3、训练/测试数据集的加载

我们先看下 train.pydata_load.pyeval.py 三个文件:

  • train.py:该文件包含了 Graph 类的定义,并在其构造函数中调用 load_data.py 文件中的 get_batch_data 函数加载训练数据。
  • data_load.py:定义了加载训练数据、加载测试数据的函数。
  • eval.py:测试结果的评价函数定义在这个文件里。

下面是函数调用的流程:

def load_de_vocab():
    vocab = [line.split()[0] for line in codecs.open('preprocessed/de.vocab.tsv', 'r', 'utf-8').read().splitlines() if int(line.split()[1])>=hp.min_cnt]
    word2idx = {
    
    word: idx for idx, word in enumerate(vocab)}
    idx2word = {
    
    idx: word for idx, word in enumerate(vocab)}
    return word2idx, idx2word

def load_en_vocab():
    vocab = [line.split()[0] for line in codecs.open('preprocessed/en.vocab.tsv', 'r', 'utf-8').read().splitlines() if int(line.split()[1])>=hp.min_cnt]
    word2idx = {
    
    word: idx for idx, word in enumerate(vocab)}
    idx2word = {
    
    idx: word for idx, word in enumerate(vocab)}
    return word2idx, idx2word

preprocessed/de.vocab.tsvpreprocessed/en.vocab.tsv 中储存的德语、英语的词汇、词频,载入成 word2idxidx2word。前者是通过词查询词向量,后者通过词向量查询词。

load_de_vocabload_en_vocab 函数被 create_data 函数引用,该函数将输入的源语言和目标语言句子转换为索引表示,并对过长的句子进行截断或填充。详细的解释看下面代码里的注释。

# 输入参数是翻译模型的源语言语句、目标语言语句
def create_data(source_sents, target_sents):

    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
    
    # 用 zip 函数将源语言和目标语言句子对应起来,并对句子进行截断或填充
    x_list, y_list, Sources, Targets = [], [], [], []
    for source_sent, target_sent in zip(source_sents, target_sents):
        x = [de2idx.get(word, 1) for word in (source_sent + u" </S>").split()] # 1: OOV, </S>: End of Text
        y = [en2idx.get(word, 1) for word in (target_sent + u" </S>").split()] 

        # 将句子的词的编号,原句以及编号后的句子存储下来,以供之后使用
        if max(len(x), len(y)) <=hp.maxlen:

        	# 将 x 和 y 转换成 numpy 数组并加入 x_list 和 y_list 中
            x_list.append(np.array(x))
            y_list.append(np.array(y))

            # 将原始的 source_sent 和 target_sent 加入 Sources 和 Targets 列表中
            Sources.append(source_sent)
            Targets.append(target_sent)
    
    # 对于每个 (x, y) 对,使用 np.lib.pad 函数将 x 和 y 分别用 0 进行填充,直到长度为 hp.maxlen
    # 这样做的目的是使得每个句子长度都相等,方便后续的训练
    X = np.zeros([len(x_list), hp.maxlen], np.int32)
    Y = np.zeros([len(y_list), hp.maxlen], np.int32)
    for i, (x, y) in enumerate(zip(x_list, y_list)):
        X[i] = np.lib.pad(x, [0, hp.maxlen-len(x)], 'constant', constant_values=(0, 0))
        Y[i] = np.lib.pad(y, [0, hp.maxlen-len(y)], 'constant', constant_values=(0, 0))

    # 返回转换后的索引表示,以及未经处理的源语言和目标语言句子
    # X 是原始句子中德语的索引
    # Y 是原始句子中英语的索引
    # Sources 是源原始句子列表,并与 X 一一对应
    # Targets 是目标原始句子列表,并与 Y 一一对应
    return X, Y, Sources, Targets

# 返回原始句子中德语、英语的索引
def load_train_data():
    de_sents = [regex.sub("[^\s\p{Latin}']", "", line) for line in codecs.open(hp.source_train, 'r', 'utf-8').read().split("\n") if line and line[0] != "<"]
    en_sents = [regex.sub("[^\s\p{Latin}']", "", line) for line in codecs.open(hp.target_train, 'r', 'utf-8').read().split("\n") if line and line[0] != "<"]
    
    X, Y, Sources, Targets = create_data(de_sents, en_sents)
    return X, Y

下面的 get_batch_data 则从文本数据中读取并生成 batch:

def get_batch_data():
    
    # 加载数据
    X, Y = load_train_data()
    
    # calc total batch count
    num_batch = len(X) // hp.batch_size
    
    # 将 X 和 Y 转换成张量
    X = tf.convert_to_tensor(X, tf.int32)
    Y = tf.convert_to_tensor(Y, tf.int32)
    
    # 创建输入队列
    input_queues = tf.train.slice_input_producer([X, Y])
            
    # 创建 batch 队列,利用 shuffle_batch 将一组 tensor 随机打乱,并将它们分为多个 batch
    # 使用 shuffle_batch 是为了防止模型过拟合
    x, y = tf.train.shuffle_batch(input_queues,
                                num_threads=8,
                                batch_size=hp.batch_size, 
                                capacity=hp.batch_size*64,   
                                min_after_dequeue=hp.batch_size*32, 
                                allow_smaller_final_batch=False)
    
    return x, y, num_batch # (N, T), (N, T), ()

15.4、构建模型并训练

Graph 的构造函数流程,就是模型的构建流程,下面船长来分析这部分代码。

整体这个流程,主要涉及 train.py 文件和 modules.py 文件。所有模型所需的主要函数定义,都是在 modules.py 中实现的。我们先看下编码器(Encoder)的流程:

下面是 train.py 中实现的 Transformer 流程,其中的每一段代码,船长都会做详细解释,先不用急。这个流程里,首先定义了编码器,先使用了 Embedding 层将输入数据转换为词向量,使用 Positional Encoding 层对词向量进行位置编码,使用 Dropout 层进行 dropout 操作,然后进行多层 Multihead Attention 和 Feed Forward 操作。

在构建模型前,先执行 train.py 的主程序段,首先 if __name__ == '__main__' 这句代码是在 Python 中常用的一种编写方式,它的意思是当一个文件被直接运行时,if 语句下面的代码会被执行。请看下面代码的注释。

if __name__ == '__main__':                
    
    # 加载词汇表   
    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
    
    # 构建模型并训练
    g = Graph("train"); print("Graph loaded")
    
    # 创建了一个 Supervisor 对象来管理训练过程
    sv = tf.train.Supervisor(graph=g.graph, 
                             logdir=hp.logdir,
                             save_model_secs=0)

    # 使用 with 语句打开一个会话
    with sv.managed_session() as sess:

    	# 训练迭代 hp.num_epochs 次
        for epoch in range(1, hp.num_epochs+1): 
            if sv.should_stop(): break

            # tqdm 是一个 Python 库,用来在循环执行训练操作时在命令行中显示进度条
            for step in tqdm(range(g.num_batch), total=g.num_batch, ncols=70, leave=False, unit='b'):

            	# 每次迭代都会运行训练操作 g.train_op
                sess.run(g.train_op)

            # 获取训练的步数,通过 sess.run() 函数获取 global_step 的当前值并赋值给 gs。这样可在后面使用 gs 保存模型时用这个值命名模型
            gs = sess.run(g.global_step)

            # 每个 epoch 结束时,它使用 saver.save() 函数保存当前模型的状态
            sv.saver.save(sess, hp.logdir + '/model_epoch_%02d_gs_%d' % (epoch, gs))
    
    print("Done")
  • num_epochs 是训练过程中迭代的次数,它表示训练模型需要在训练数据上跑多少遍。每一次迭代都会在训练数据集上进行训练,通常来说,训练数据集会被重复多次迭代,直到达到 num_epochs 次。这样可以确保模型能够充分地学习数据的特征。设置 num_epochs 的值过大或过小都会导致模型性能下降。
15.4.1、编码过程
Embedding

embedding 用来把输入生成词嵌入向量:

# 词语转换为对应的词向量表示
self.enc = embedding(self.x, 
                      vocab_size=len(de2idx), 
                      num_units=hp.hidden_units, 
                      scale=True,
                      scope="enc_embed")
  • vocab_size 是词汇表的大小。
  • num_units 是词向量的维度。
  • scale 是一个布尔值,用来确定是否对词向量进行标准化。
  • scope 是变量作用域的名称。
Key Masks

接着生成一个 key_masks 用于在之后的计算中屏蔽掉某些位置的信息,以便模型只关注有效的信息。

key_masks = tf.expand_dims(tf.sign(tf.reduce_sum(tf.abs(self.enc), axis=-1)), -1)
  • 先对 self.enc 张量进行对每个元素求绝对值的操作
  • 沿着最后一阶作为轴,进行 reduce_sum 操作,得到一个 (batch, sequence_length) 形状的张量。
  • 再进行 tf.sign 操作,对刚得到的每个元素进行符号函数的变换。
  • 最后再扩展阶数,变成形状 (batch, sequence_length, 1) 的张量。
Positional Encoding

下面生成 Transformer 的位置编码:

# 位置编码
if hp.sinusoid:
    self.enc += positional_encoding(self.x,
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="enc_pe")
else:
    self.enc += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.x)[1]), 0),
    							 [tf.shape(self.x)[0], 1]),
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="enc_pe")

如果超参数 hp.sinusoid=True,使用 positional_encoding 函数,通过使用正弦和余弦函数来生成位置编码,可以为输入序列添加位置信息。如果 hp.sinusoid=False,使用 embedding 函数,通过学习的词嵌入来生成位置编码。

位置编码生成后,用 key_masks 处理一下。注意 key_masks 的生成一定要用最初的 self.enc,所以在前面执行而不是这里:

self.enc *= key_masks

这个不是矩阵乘法,而是对应元素相乘。这里乘上 key_masks 的目的是将 key_masks 中值为 0 的位置对应的 self.enc 中的元素置为 0,这样就可以排除这些位置对计算的影响。

Drop out

下面调用了 TensorFlow 的 drop out 操作:

self.enc = tf.layers.dropout(self.enc, 
                            rate=hp.dropout_rate, 
                            training=tf.convert_to_tensor(is_training))

drop out 是一种在深度学习中常用的正则化技巧。它通过在训练过程中随机地「关闭」一些神经元来减少 过拟合。这样做是为了防止模型过于依赖于某些特定的特征,而导致在新数据上的表现不佳。

在这个函数中,dropout 层通过在训练过程中随机地将一些神经元的输出值设置为 0,来减少模型的过拟合。这个函数中使用了一个参数 rate,表示每个神经元被「关闭」的概率。这样做是为了防止模型过于依赖于某些特定的特征,而导致在新数据上的表现不佳。

Encoder Blocks: Multi-Head Attention & Feed Forward

然后看下 encoder blocks 代码:

## Blocks
for i in range(hp.num_blocks):
    with tf.variable_scope("num_blocks_{}".format(i)):
        # 多头注意力
        self.enc = multihead_attention(queries=self.enc, 
                                        keys=self.enc, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads, 
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training,
                                        causality=False)
        
        # 前馈神经网络
        self.enc = feedforward(self.enc, num_units=[4*hp.hidden_units, hp.hidden_units])

上述代码是编码器(Encoder)的实现函数调用的流程,也是与麦克船长上面的模型原理介绍一致的,在定义时同样使用了 Embedding 层、Positional Encoding 层、Dropout 层、Multihead Attention 和 Feed Forward 操作。其中 Multihead Attention 在编码、解码中是不一样的,待会儿我们会在 Decoder 部分再提到,有自注意力层和 Encoder-Decoder 层。

  • 超参数 hp.num_blocks 表示 Encoder Blocks 的层数,每一层都有一个 Multi-Head Attention 和一个 Feed Forward。
  • 这个 Encoder 中的 Multi-Head Attention 是基于自注意力的(注意与后面的 Decoder 部分有区别)
  • causality 参数的意思是否使用 Causal Attention,它是 Self-Attention 的一种,但是只使用过去的信息,防止模型获取未来信息的干扰。一般对于预测序列中的某个时间步来说,只关注之前的信息,而不是整个序列的信息。这段代码中 causality 设置为了 False,即会关注整个序列的信息。
15.4.2、解码过程

再看一下解码的流程:

Embedding

下面我们逐一看每段代码,主要关注与编码阶段的区别即可:

self.dec = embedding(self.decoder_inputs, 
                      vocab_size=len(en2idx), 
                      num_units=hp.hidden_units,
                      scale=True, 
                      scope="dec_embed")
  • embedding 输入用的是 self.decoder_inputs
  • 词汇表尺寸用翻译后的输出语言英语词汇表长度 len(en2idx)
Key Masks
key_masks = tf.expand_dims(tf.sign(tf.reduce_sum(tf.abs(self.dec), axis=-1)), -1)
  • key_masks 输入变量用 self.dec
Positional Encoding & Drop out
# 位置编码
if hp.sinusoid:
    self.dec += positional_encoding(self.decoder_inputs,
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="dec_pe")
else:
    self.dec += embedding(tf.tile(tf.expand_dims(tf.range(tf.shape(self.decoder_inputs)[1]), 0),
    							 [tf.shape(self.decoder_inputs)[0], 1]),
                      vocab_size=hp.maxlen, 
                      num_units=hp.hidden_units, 
                      zero_pad=False, 
                      scale=False,
                      scope="dec_pe")

self.dec *= key_masks

self.dec = tf.layers.dropout(self.dec, 
                            rate=hp.dropout_rate, 
                            training=tf.convert_to_tensor(is_training))
  • 输入 self.decoder_inputs
  • 指定 vocab_size 参数 hp.maxlen
Decoder Blocks: Multi-Head Attention & Feed Forward
## 解码器模块
for i in range(hp.num_blocks):
    with tf.variable_scope("num_blocks_{}".format(i)):
        # 多头注意力(自注意力)
        self.dec = multihead_attention(queries=self.dec, 
                                        keys=self.dec, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads, 
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training,
                                        causality=True, 
                                        scope="self_attention")
        
        # 多头注意力(Encoder-Decoder 注意力)
        self.dec = multihead_attention(queries=self.dec, 
                                        keys=self.enc, 
                                        num_units=hp.hidden_units, 
                                        num_heads=hp.num_heads,
                                        dropout_rate=hp.dropout_rate,
                                        is_training=is_training, 
                                        causality=False,
                                        scope="vanilla_attention")

        # 前馈神经网络
        self.dec = feedforward(self.dec, num_units=[4*hp.hidden_units, hp.hidden_units])
  • 在用 multihead_attention 函数解码器模块时,注意传入的参数 scope 区别,先是自注意力层,用参数 self_attention,对应的 queriesself.deckeys 也是 self.dec。再是「Encoder-Decder Attention」用的是参数 vanilla_attention,对应的 queries 来自解码器是 self.dec,但 keys 来自编码器是是 self.enc
15.4.3、Incorporação、Codificação Posicional、Atenção Multi-Head、Feed Forward
Incorporando a implementação da função
def embedding(inputs, 
              vocab_size, 
              num_units, 
              zero_pad=True, 
              scale=True,
              scope="embedding", 
              reuse=None):
    with tf.variable_scope(scope, reuse=reuse):

    	# 创建一个名为 `lookup_table`、形状为 (vocab_size, num_units) 的矩阵
        lookup_table = tf.get_variable('lookup_table',
                                       dtype=tf.float32,
                                       shape=[vocab_size, num_units],
                                       initializer=tf.contrib.layers.xavier_initializer())

        # lookup_table 的第一行插入一个全零行,作为 PAD 的词向量
        if zero_pad:
            lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
                                      lookup_table[1:, :]), 0)

        # 在词向量矩阵 lookup_table 中查找 inputs
        outputs = tf.nn.embedding_lookup(lookup_table, inputs)
        
        # 对输出的词向量进行除以根号 num_units 的操作,可以控制词向量的统计稳定性。
        if scale:
            outputs = outputs * (num_units ** 0.5) 
            
    return outputs
Implementação da função de codificação posicional
def positional_encoding(inputs,
                        num_units,
                        zero_pad=True,
                        scale=True,
                        scope="positional_encoding",
                        reuse=None):

    N, T = inputs.get_shape().as_list()
    with tf.variable_scope(scope, reuse=reuse):

    	# tf.range(T) 生成一个 0~T-1 的数组
    	# tf.tile() 将其扩展成 N*T 的矩阵,表示每个词的位置
        position_ind = tf.tile(tf.expand_dims(tf.range(T), 0), [N, 1])

        # First part of the PE function: sin and cos argument
        position_enc = np.array([
            [pos / np.power(10000, 2.*i/num_units) for i in range(num_units)]
            for pos in range(T)])

        # 用 numpy 的 sin 和 cos 函数对每个位置进行编码
        position_enc[:, 0::2] = np.sin(position_enc[:, 0::2])  # dim 2i
        position_enc[:, 1::2] = np.cos(position_enc[:, 1::2])  # dim 2i+1

        # 将编码结果转为张量
        lookup_table = tf.convert_to_tensor(position_enc)

        # 将编码的结果与位置索引相关联,得到最终的位置编码
        if zero_pad:
        	# 如果 zero_pad 参数为 True,则在编码结果的开头添加一个全 0 的向量
            lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
                                      lookup_table[1:, :]), 0)
        outputs = tf.nn.embedding_lookup(lookup_table, position_ind)

        # scale 参数为 True,则将编码结果乘上 num_units 的平方根
        if scale:
            outputs = outputs * num_units**0.5

        return outputs
Implementação da função Multi-Head Attention
def multihead_attention(queries, 
                        keys, 
                        num_units=None, 
                        num_heads=8, 
                        dropout_rate=0,
                        is_training=True,
                        causality=False,
                        scope="multihead_attention", 
                        reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
        # Set the fall back option for num_units
        if num_units is None:
            num_units = queries.get_shape().as_list()[-1]
        
        # Linear Projections
        # 使用三个全连接层对输入的 queries、keys 分别进行线性变换,将其转换为三个维度相同的张量 Q/K/V
        Q = tf.layers.dense(queries, num_units, activation=tf.nn.relu) # (N, T_q, C)
        K = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)
        V = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)
        
        # Split and concat
        # 按头数 split Q/K/V,再各自连接起来
        Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*N, T_q, C/h) 
        K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*N, T_k, C/h) 
        V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*N, T_k, C/h) 

        # Multiplication
        # 计算 Q_, K_, V_ 的点积来获得注意力权重
        # 其中 Q_ 的维度为 (hN, T_q, C/h)
        # K_ 的维度为 (hN, T_k, C/h)
        # 计算出来的结果 outputs 的维度为 (h*N, T_q, T_k)
        outputs = tf.matmul(Q_, tf.transpose(K_, [0, 2, 1])) # (h*N, T_q, T_k)

        # Scale
        # 对权重进行 scale,这里除以了 K_ 的第三维的平方根,用于缩放权重
        outputs = outputs / (K_.get_shape().as_list()[-1] ** 0.5)
        
        # Key Masking
        # 这里需要将 keys 的有效部分标记出来,将无效部分设置为极小值,以便在之后的 softmax 中被忽略
        key_masks = tf.sign(tf.reduce_sum(tf.abs(keys), axis=-1)) # (N, T_k)
        key_masks = tf.tile(key_masks, [num_heads, 1]) # (h*N, T_k)
        key_masks = tf.tile(tf.expand_dims(key_masks, 1), [1, tf.shape(queries)[1], 1]) # (h*N, T_q, T_k)
        
        paddings = tf.ones_like(outputs)*(-2**32+1)
        outputs = tf.where(tf.equal(key_masks, 0), paddings, outputs) # (h*N, T_q, T_k)
  
        # Causality = Future blinding
        if causality:

        	# 创建一个与 outputs[0, :, :] 相同形状的全 1 矩阵
            diag_vals = tf.ones_like(outputs[0, :, :]) # (T_q, T_k)

            # 对 diag_vals 进行处理,返回一个下三角线矩阵
            tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense() # (T_q, T_k)
            masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1]) # (h*N, T_q, T_k)
   
   			# 将 masks 为 0 的位置的 outputs 值设置为一个非常小的数
   			# 这样会导致这些位置在之后的计算中对结果产生非常小的影响,从而实现了遮盖未来信息的功能
            paddings = tf.ones_like(masks)*(-2**32+1)
            outputs = tf.where(tf.equal(masks, 0), paddings, outputs) # (h*N, T_q, T_k)
  
        # 对于每个头的输出,应用 softmax 激活函数,这样可以得到一个概率分布
        outputs = tf.nn.softmax(outputs) # (h*N, T_q, T_k)
         
        # Query Masking
        # 对于查询(queries)进行 masking,这样可以避免输入序列后面的词对之前词的影响
        query_masks = tf.sign(tf.reduce_sum(tf.abs(queries), axis=-1)) # (N, T_q)
        query_masks = tf.tile(query_masks, [num_heads, 1]) # (h*N, T_q)
        query_masks = tf.tile(tf.expand_dims(query_masks, -1), [1, 1, tf.shape(keys)[1]]) # (h*N, T_q, T_k)
        outputs *= query_masks # broadcasting. (N, T_q, C)
          
        # Dropouts & Weighted Sum
        # 对于每个头的输出,应用 dropout 以及进行残差连接
        outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=tf.convert_to_tensor(is_training))
        outputs = tf.matmul(outputs, V_) # ( h*N, T_q, C/h)
        
        # Restore shape
        # 将每个头的输出拼接起来,使用 tf.concat 函数,将不同头的结果按照第二维拼接起来
        # 得到最终的输出结果,即经过多头注意力计算后的结果
        outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # (N, T_q, C)
              
        # Residual connection
        outputs += queries
              
        # Normalize
        outputs = normalize(outputs) # (N, T_q, C)
 
    return outputs
Implementação da função Feed Forward

A seguir está a definição da camada de rede neural feedforward. Esta é uma transformação não linear. Algum conhecimento de rede neural convolucional (CNN) é usado aqui. Vamos ver o código e explicá-lo:

def feedforward(inputs, 
                num_units=[2048, 512],
                scope="multihead_attention", 
                reuse=None):
    with tf.variable_scope(scope, reuse=reuse):
        # Inner layer
        params = {
    
    "inputs": inputs, "filters": num_units[0], "kernel_size": 1,
                  "activation": tf.nn.relu, "use_bias": True}
        outputs = tf.layers.conv1d(**params)
        
        # Readout layer
        params = {
    
    "inputs": outputs, "filters": num_units[1], "kernel_size": 1,
                  "activation": None, "use_bias": True}
        outputs = tf.layers.conv1d(**params)
        
        # 连接一个残差网络 ResNet
        outputs += inputs
        
        # 归一化后输出
        outputs = normalize(outputs)
    
    return outputs
  • Primeiro, uma camada convolucional (conv1d) é usada como a camada interna, uma camada convolucional é usada como a camada de leitura e o tamanho do kernel de convolução é 1.
  • filtersO parâmetro é usado para controlar o número de canais de saída na camada convolucional. O número de canais de saída da camada interna é definido como num_units[0], e o da camada de leitura é definido como num_units[1]. Às vezes, isso também é interpretado como o número de neurônios. Os padrões desses dois são 2048 e 512, respectivamente, e os hiperparâmetros são passados ​​ao chamar [4 * hidden_units, hidden_units].
  • A camada interna é usada ReLUcomo a função de ativação e, em seguida, uma rede residual RedNet é conectada e a saída da camada de leitura é adicionada à entrada original.
  • Por fim, normalizeuse normalizar para processar a saída e, em seguida, retornar. Vejamos normalizea função .
def normalize(inputs, 
              epsilon = 1e-8,
              scope="ln",
              reuse=None):
    with tf.variable_scope(scope, reuse=reuse):

    	# 输入数据的形状
        inputs_shape = inputs.get_shape()
        params_shape = inputs_shape[-1:]
    
    	# 平均数、方差
        mean, variance = tf.nn.moments(inputs, [-1], keep_dims=True)

        # 拉伸因子 beta
        beta= tf.Variable(tf.zeros(params_shape))

        # 缩放因子 gamma
        gamma = tf.Variable(tf.ones(params_shape))

        # 归一化:加上一个非常小的 epsilon,是为了防止除以 0
        normalized = (inputs - mean) / ( (variance + epsilon) ** (.5) )

        outputs = gamma * normalized + beta
        
    return outputs
  • Esta função implementa a normalização de camada, que é usada para resolver o problema de instabilidade de dados em redes neurais profundas.
15.4.4. Operação após codificação e decodificação

Após o decodificador Linear & Softmax:

# 全连接层得到的未经过归一化的概率值
self.logits = tf.layers.dense(self.dec, len(en2idx))

# 预测的英文单词 idx
self.preds = tf.to_int32(tf.arg_max(self.logits, dimension=-1))
self.istarget = tf.to_float(tf.not_equal(self.y, 0))

# 正确预测数量,除以所有样本数,得到准确率
self.acc = tf.reduce_sum(tf.to_float(tf.equal(self.preds, self.y))*self.istarget)/ (tf.reduce_sum(self.istarget))

#  记录了模型的准确率的值,用于 tensorboard 可视化
tf.summary.scalar('acc', self.acc)

Ao processar os dados do conjunto de treinamento, o processamento Linear & Softmaxfinal é o seguinte. tf.nn.softmax_cross_entropy_with_logitsA perda de entropia cruzada é usada aqui para calcular a taxa de erro do modelo mean_loss, e o otimizador Adam é usado AdamOptimizerpara otimizar os parâmetros do modelo.

# 使用 label_smoothing 函数对真实标签进行标签平滑,得到 self.y_smoothed
self.y_smoothed = label_smoothing(tf.one_hot(self.y, depth=len(en2idx)))

O código a seguir implementa uma técnica chamada "label Smoothing".

def label_smoothing(inputs, epsilon=0.1):

	# 获取输入的类别数,并将其赋值给变量 K
    K = inputs.get_shape().as_list()[-1] # number of channels
    return ((1-epsilon) * inputs) + (epsilon / K)

Durante o processo de treinamento, o rótulo da amostra é representado como uma matriz bidimensional, onde a primeira dimensão representa o número da amostra e a segunda dimensão representa o rótulo da amostra. A forma desta matriz é (número de amostras, número de categorias), então o número de categorias corresponde à última dimensão. Especificamente neste caso de uso de modelo, a primeira dimensão é o número de frases de exemplo em alemão e a última dimensão é o tamanho do vocabulário em inglês.

É usado para resolver o problema de overfitting que ocorre ao treinar o modelo. Na suavização de rótulos, adicionamos algum ruído ao rótulo de cada amostra, para que o modelo não possa ser treinado completamente dependente do rótulo da amostra, reduzindo assim a possibilidade de overfitting. Especificamente, esse código inputsmultiplica por 1-epsilon, mais epsilon / K, onde epsiloné o fator de suavização e Ké o número de categorias de rótulos (tamanho do vocabulário em inglês). Isso pode tornar a previsão do rótulo do modelo mais estável durante o processo de treinamento e reduzir o risco de overfitting.

Então, olhamos para as operações de acompanhamento.

# 对于分类问题来说,常用的损失函数是交叉熵损失
self.loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=self.y_smoothed)
self.mean_loss = tf.reduce_sum(self.loss * self.istarget) / (tf.reduce_sum(self.istarget))

# Training Scheme
self.global_step = tf.Variable(0, name='global_step', trainable=False)

# Adam 优化器 self.optimizer,用于优化损失函数
self.optimizer = tf.train.AdamOptimizer(learning_rate=hp.lr, beta1=0.9, beta2=0.98, epsilon=1e-8)

# 使用优化器的 minimize() 函数创建一个训练操作 self.train_op,用于更新模型参数。这个函数会自动计算梯度并应用更新
self.train_op = self.optimizer.minimize(self.mean_loss, global_step=self.global_step)
   
# 将平均损失写入 TensorFlow 的 Summary 中,用于 tensorboard 可视化
tf.summary.scalar('mean_loss', self.mean_loss)

# 将所有的 summary 合并到一起,方便在训练过程中写入事件文件
self.merged = tf.summary.merge_all()

15.5. Avaliação do efeito

def eval(): 
    # 创建一个处理测试数据集的 Graph 实例
    g = Graph(is_training=False)
    print("Graph loaded")
    
    # 加载测试数据
    X, Sources, Targets = load_test_data()
    de2idx, idx2de = load_de_vocab()
    en2idx, idx2en = load_en_vocab()
     
    # Start session         
    with g.graph.as_default():

    	# TensorFlow 中用于管理训练的一个类
    	# 它可以帮助你轻松地管理训练过程中的各种资源,如模型参数、检查点和日志
        sv = tf.train.Supervisor()

        # 创建一个会话
        with sv.managed_session(config=tf.ConfigProto(allow_soft_placement=True)) as sess:

            # 恢复模型参数
            sv.saver.restore(sess, tf.train.latest_checkpoint(hp.logdir))
            print("Restored!")
              
            # 获取模型名称
            mname = open(hp.logdir + '/checkpoint', 'r').read().split('"')[1] # model name
             
            ## Inference
            if not os.path.exists('results'): os.mkdir('results')

            # 初始化结果文件
            with codecs.open("results/" + mname, "w", "utf-8") as fout:
                list_of_refs, hypotheses = [], []

                # 循环处理数据
                for i in range(len(X) // hp.batch_size):
                     
                    # 获取小批量数据
                    x = X[i*hp.batch_size: (i+1)*hp.batch_size]
                    sources = Sources[i*hp.batch_size: (i+1)*hp.batch_size]
                    targets = Targets[i*hp.batch_size: (i+1)*hp.batch_size]
                     
                    # 使用自回归推理(Autoregressive inference)得到预测结果
                    preds = np.zeros((hp.batch_size, hp.maxlen), np.int32)
                    for j in range(hp.maxlen):
                        _preds = sess.run(g.preds, {
    
    g.x: x, g.y: preds})
                        preds[:, j] = _preds[:, j]
                     
                    # 将预测结果写入文件
                    for source, target, pred in zip(sources, targets, preds): # sentence-wise
                        got = " ".join(idx2en[idx] for idx in pred).split("</S>")[0].strip()
                        fout.write("- source: " + source +"\n")
                        fout.write("- expected: " + target + "\n")
                        fout.write("- got: " + got + "\n\n")
                        fout.flush()
                          
                        # bleu score
                        ref = target.split()
                        hypothesis = got.split()
                        if len(ref) > 3 and len(hypothesis) > 3:
                            list_of_refs.append([ref])
                            hypotheses.append(hypothesis)
              
                # 计算 BLEU 分数,并将其写入文件
                score = corpus_bleu(list_of_refs, hypotheses)
                fout.write("Bleu Score = " + str(100*score))
                                          
if __name__ == '__main__':
    eval()
    print("Done")

Seção 16 Desempenho do Kyubyong Transformer e alguns problemas

A última linha do arquivo de resultado da avaliação Bleu Score = 6.598452846670836indica que o resultado da tradução desse modelo de tradução tem um grau relativamente alto de sobreposição com a tradução de referência e a qualidade da tradução é boa. No entanto, deve-se notar que a pontuação BLEU não pode refletir totalmente a qualidade da tradução, porque não pode avaliar problemas de gramática, semântica, entonação, etc.

Além disso, salvamos os dados do processo no logdir no código anterior, apenas para facilitar a visualização subsequente, podemos usar o TensorBoard para visualizar, o método de uso específico é o seguinte:

mikecaptain@local $ tensorboard --logdir logdir

Em seguida, verifique no navegador http://localhost:6006, o exemplo é o seguinte:

Olhando para trás na implementação do código de todo o modelo, podemos ver de forma mais intuitiva que este Transformer pode capturar melhor as dependências de longa distância e melhorar a qualidade da tradução. No entanto, existem alguns problemas com a implementação do Kyubyong Transformer. O modelo Transformer também precisa ajustar muitos hiperparâmetros durante o processo de treinamento, como taxa de aprendizado (learning rate), tamanho do lote, etc. Diferentes tarefas podem exigir diferentes ajustes de hiperparâmetros.

Fim · Anos depois de Transformer

As vantagens do Transformer são óbvias:

  • Mais rápido - Bom paralelismo: Antes do Transformer nascer, o RNN era o modelo principal no campo de NLP, mas o RNN tem um paralelismo ruim (processamento serial de sequência).
  • Não esquecido - distância da palavra reduzida para 1: o modelo RNN manipula o conteúdo de texto longo que foi perdido (o que significa longa distância espacial das palavras no modelo RNN).
  • Manipulando sequências de comprimentos variados: Sequências que não requerem que os dados de entrada sejam de comprimento fixo.
  • Facilidade de aprendizado por transferência.

Portanto, o modelo baseado no princípio Transformer alcançou excelente desempenho em muitas tarefas de PNL.

Afinal, a área de aprendizado de máquina (Machine Learning) ainda é uma ciência experimental, e é uma ciência experimental muito próxima da indústria. A perspectiva do aprendizado de máquina para visualizar resultados experimentais não é resumir e abstrair resultados experimentais para promover o desenvolvimento da ciência teórica. Os resultados experimentais do aprendizado de máquina devem ser avaliados e existem padrões de avaliação quantitativa objetiva para seus efeitos. Portanto, aprendizado de máquina, tudo fala com resultados. O modelo grande GPT da OpenAI nasceu com base na parte Decoder da arquitetura Transformer, e o modelo grande BERT do Google nasceu com base na parte Encoder de sua arquitetura. Ambos nasceram em 2018. Nos últimos anos, surgiram várias ideias de otimização baseadas no Transformer, e o mestre é o ChatGPT baseado no GPT-3.5 ou o InstructGPT no final de 2022.

Obrigado por sua paciência em ler este artigo de quase 80.000 palavras. Por se tratar de notas técnicas do capitão, os pontos-chave foram resolvidos em detalhes. A seguir, falarei com você sobre a situação atual do AIGC. Se o conteúdo deste artigo for mais como um tutorial (estudo aprofundado da tecnologia de origem), nossa discussão de acompanhamento pode ser mais como um relatório (para o atual desenvolvimento acadêmico e da indústria. visão geral do status quo), daremos mais atenção aos dois tópicos na parte "Prefácio" do artigo: 1) Se passar no teste de Turing representa AGI (Artificial General Intelligence, inteligência artificial geral), qual é o desenvolvimento atual da PNL e até da AGI? 2) Qual pode ser a rota de desenvolvimento da AGI nos próximos anos?

A IA acabará subvertendo todas as esferas da vida. Todas as pessoas têm a responsabilidade de passar algum tempo prestando atenção ao pulso do desenvolvimento de ponta. Você pode se comigo no DingTalk ou WeChat (id: sinosuperman).

Finalmente, o capitão Mike desejou a todos um feliz ano novo e desejou a todos saúde e felicidade no ano do coelho.

referência

  • https://web.stanford.edu/~jurafsky/slp3/3.pdf
  • https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html
  • "Processamento de linguagem natural: um método baseado em modelos de pré-treinamento" Che Wanxiang et al.
  • https://cs.stanford.edu/people/karpathy/convnetjs/
  • https://arxiv.org/abs/1706.03762
  • https://arxiv.org/abs/1512.03385
  • https://github.com/Kyubyong/transformer/
  • http://jalammar.github.io/illustrated-transformer/
  • https://towardsdatascience.com/this-is-how-to-train-better-transformer-models-d54191299978
  • "Processamento de Linguagem Natural na Prática: Aplicação e Produtividade do Modelo de Pré-treinamento" por Anku A. Patel et al.
  • https://lilianweng.github.io/posts/2018-06-24-attention/
  • https://github.com/lilianweng/transformer-tensorflow/
  • "Previsão de Sequência de Tempo-Espaço de Estado de Tráfego Rodoviário de Curto Prazo Baseada em Deep Learning" Cui Jianxun
  • https://www.zhihu.com/question/325839123
  • https://luweikxy.gitbook.io/machine-learning-notes/self-attention-and-transformer
  • Aprendizado Profundo com Python (2ª Edição) por François Cholet
  • https://en.wikipedia.org/wiki/Attention_(machine_learning)
  • https://zhuanlan.zhihu.com/p/410776234
  • https://www.tensorflow.org/tensorboard/get_started
  • https://paperswithcode.com/method/multi-head-attention
  • https://zhuanlan.zhihu.com/p/48508221
  • https://www.joshbelanich.com/self-attention-layer/
  • https://learning.rasa.com/transformers/kvq/
  • http://deeplearning.stanford.edu/tutorial/supervised/ConvolutionalNeuralNetwork/
  • https://zhuanlan.zhihu.com/p/352898810
  • https://towardsdatascience.com/beautifully-illustrated-nlp-models-from-rnn-to-transformer-80d69faf2109
  • https://medium.com/analytics-vidhya/understanding-qkv-in-transformer-self-attention-9a5eddaa5960

Acho que você gosta

Origin blog.csdn.net/Poechant/article/details/128878358
Recomendado
Clasificación