A Temida Matemática
por Frank de Alcantara em 09/02/2025
Nesta série, a curiosa leitora irá enfrentar os Transformers. E vencerá. Mas, tenho que admitir, não há qualquer relação com o Optimus Prime. Este transformer você não venceria. Se for estes Transformers que está procurando, o Google falhou com você!
Índice da Série: Transformers
- 1. Você Pensa Como Fala
- 2. A Temida Matemática (Você está aqui)
- 3. A Vetorização Básica
- 4. Redes Neurais Artificiais para Word Embedding
- 5. Embeddings Distribuídos e CBoW
- 6. SkipGram e Otimizações do Word2Vec
- 7. Word2Vec, a Ponte para o Contexto
- 8. Desvendando a Modelagem de Sequências
- 9. Prestando Atenção
- 10. Do Código à Geração
- 11. O Cisma e a Batalha da Eficiência
- 12. GEMM o Coração Matemático
Aqui vamos discutir os Transformers que são modelos de aprendizado de máquina que revolucionaram o processamento de linguagem natural (NLP, de Natural Language Processing). Estas técnicas, e algoritmos, foram apresentadas ao mundo em um artigo intitulado Attention is All You Need (Atenção é Tudo que Você Precisa), publicado em 20171 na conferência Advances in Neural Information Processing Systems (NeurIPS). Observe, atenta leitora, que isso se deu há quase 10 anos. No ritmo atual, uma eternidade.
O entendimento da linguagem natural por máquinas é um desafio importante que beirava o impossível. Hoje, este problema parece estar resolvido. Se isso for verdade, terá sido graças às técnicas e aos algoritmos criados em torno de aprendizado de máquina, fundamentado em cálculo e estatística. Ou se preferir, podemos dizer que usamos algoritmos determinísticos para aplicar técnicas estocásticas em bases de dados gigantescas e assim, romper os limites que haviam sido impostos pela linguística matemática e computacional determinísticas. Nos dois casos, a matemática reina absoluta.
Veremos como esses modelos, inicialmente projetados para tradução automática, se tornaram a base para tarefas como geração de texto e conversação, como no GPT-3, compreensão de linguagem em texto e até mesmo em áudio.
Será uma longa, longa viagem. Que começa com a definição das ferramentas mínimas necessárias para entender este universo.
Para criar um conjunto sólido de capacidades cognitivas, começaremos com as técnicas de representação mais simples e os conceitos matemáticos fundamentais: vetores, produtos escalares e multiplicação de matrizes. E, gradualmente, construiremos nosso entendimento.
O que suporta todo este esforço é a esperança de que a esforçada leitora possa acompanhar o raciocínio e entender como os Transformers funcionam a partir do seu cerne. E, com esta base sólida, possa criar suas próprias soluções.
Não posso esquecer dos exemplos. O combinado será o seguinte: aqui eu faço em C++20 ou C++23. Depois, a leitora faz em Python, C, JavaScript, Haskell, BrainFuck ou qualquer linguagem que desejar. Se estiver de acordo continuamos. Se não, foi uma honra ter escrito para você.
Se continuou, vamos falar de computadores.
Para que os computadores processem e compreendam a linguagem humana, é essencial converter texto em representações numéricas. Esse treco burro que controla toda a civilização humana só entende números binários inteiros. Toda e qualquer informação manipulada por um computador precisa estar dentro do conjunto dos números binários inteiros. Sim, isso é verdade para texto, áudio, vídeo, números reais e tudo o mais que você imaginar.
Dito isso, seremos obrigados a, de alguma forma, mapear o conjunto dos termos que representam uma linguagem natural no conjunto dos binários. Ou, em outras palavras, o problema é: temos que representar a linguagem, na forma de textos, em uma forma matemática (números, relações, funções) que os computadores possam manipular.
Graças a Deus, a relação entre os números binários inteiros e os números decimais inteiros é biunívoca. Mesmo que isso não seja verdade para números reais, caso em que a Norma IEEE 754 quebra a bijeção e cria uma relação de aproximação discreta e mapeamento estrutural parcial, ainda podemos entender a matemática necessária para transformar, linguagem natural em matemática usando números na base 10.
A técnica que usaremos para levar palavras, e conceitos linguísticos, a um domínio matemático é chamada de vetorização. Vamos transformar linguagem natural em vetores.
1. Vetores, os compassos de tudo que há e haverá
Eu usei exatamente a frase do título desta seção em um texto de eletromagnetismo que agora não está mais online. A ideia, naquele momento, era explicar os fenômenos do eletromagnetismo a partir da matemática. Naquele texto, como em todos os textos de eletromagnetismo, havia uma definição detalhada dos vetores e todas as suas operações. Aqui, posso ser um tanto mais direto. Muito mais discreto. Para começar a atenta leitora precisa ter em mente: vetores são os artefatos matemáticos que usamos para explicar o universo.
Um vetor é uma entidade matemática que possui tanto magnitude, ou comprimento, quanto direção. Ora chamamos de magnitude, ora chamamos de comprimento. Depende da ciência que está usando esta ferramenta. A direção é sempre direção.
Um vetor pode ser definido como um segmento de reta direcionado na geometria, ou uma sequência ordenada de números, chamados de componentes, na álgebra. A representação depende do contexto. Aqui, vamos nos concentrar na representação algébrica, que é mais comum em programação e computação.
Na geometria, um vetor pode ser visualizado como uma seta em um espaço, por exemplo, em um plano, $2D$, ou em um espaço, $3D$. O comprimento da seta representa a magnitude, e a direção da seta indica a direção do vetor. Imagine uma seta apontando para cima e para a direita em um plano. Essa seta é um vetor com uma certa magnitude (o comprimento da seta) e uma direção ($45$ graus em relação ao eixo horizontal, por exemplo). No artefato abaixo, a curiosa leitora pode manipular um vetor de verdade em vez de olhar para uma seta congelada. Ajuste os dois controles, a direção $\theta$ e a magnitude $\vert v \vert$, e observe como as componentes $v_1$ e $v_2$, projetadas sobre os eixos $(x,y)$, acompanham cada mudança; repare que a fórmula da magnitude recalcula o comprimento a partir das componentes a cada ajuste, e que os vetores unitários $\hat{i}$ e $\hat{j}$ marcam a escala dos eixos. O vetor começa exatamente como neste exemplo: magnitude $1$ e ângulo $\theta$ de $45$ graus.
Em um sistema algébrico de coordenadas, um vetor pode ser representado como uma tupla. Por exemplo, em um espaço tridimensional, um vetor pode ser escrito como $(x, y, z)$, na qual $x$, $y$ e $z$ são as componentes do vetor projetadas ao longo dos eixos $x$, $y$ e $z$, respectivamente. Assim, se nos limitarmos a $2D$ para tornar a visualização mais simples, o vetor $(2, 3)$ representa um deslocamento de $2$ unidades na direção $x$ e $3$ unidades na direção $y$. No laboratório acima podemos ver um vetor $v$ partindo da origem $O$ e terminando no ponto $P(v_1, v_2)$ onde $v_1$ e $v_2$ são, respectivamente as projeções sobre os eixos $x$ e $y$.
1.1 Espaço Vetorial
Para compreender vetores e suas operações, precisamos primeiro entender um conceito algébrico que permite que tudo se encaixe. O conceito de espaço vetorial.
Um espaço vetorial é uma estrutura matemática que formaliza a noção de operações geométricas como adição de vetores e multiplicação por escalares.
Formalmente, um espaço vetorial sobre um corpo $F$ é um conjunto $V$ no qual há adição de vetores e multiplicação por escalares em $F$, obedecendo axiomas que garantem associatividade, comutatividade, existência de neutro e inverso aditivo, além de compatibilidade entre multiplicação por escalar e estrutura do corpo.
Em processamento de linguagem natural, trabalharemos principalmente com o espaço vetorial real $\mathbb{R}^n$, na qual $n$ representa a dimensão do espaço vetorial. Ou, em linguagem de gente, quantos itens teremos no nosso vetor. Logo, $\mathbb{R}^n$ representa o espaço vetorial que contém todas as $n$-tuplas ordenadas de números reais. Formalmente, em linguagem matemática, definimos $\mathbb{R}^n$ como:
\[\mathbb{R}^n = \{(x_1, \ldots, x_n) : x_i \in \mathbb{R} \text{ para } i = 1, \ldots, n\}\]Quando representarmos palavras (termos), ou documentos, como vetores, estaremos mapeando elementos linguísticos para pontos em um espaço dado por $\mathbb{R}^n$. Neste domínio de conhecimento, a dimensão $n$ será determinada pelo método específico de vetorização que escolhermos.
Guarde esse conceito: a dimensão $n$ será determinada pelo método específico de vetorização que escolhermos.
Ao converter textos e elementos linguísticos em representações vetoriais, criamos word embeddings. Vamos chamar de word embeddings as técnicas que mapeiam palavras ou frases para vetores de números reais. Esta representação tenta capturar tanto o significado semântico quanto as relações contextuais entre palavras em um espaço vetorial contínuo.
Embeddings são representações vetoriais densas de palavras ou frases, onde cada dimensão captura uma característica semântica ou gramatical.
Percebeu? As dimensões capturam características semânticas.
A esforçada leitora irá aprender, ao longo da série, que um desenvolvimento importante no campo do processamento de linguagem natural está sintetizado nos Mecanismos de Atenção, que utilizam vetores de consulta (query), chave (key) e valor (value) como componentes essenciais. Estes mecanismos constituem o núcleo da arquitetura dos Transformers, permitindo que o modelo pondere a importância relativa de diferentes elementos em uma sequência, melhorando significativamente a capacidade de processamento de dependências de longo alcance em textos.
Para que a leitora chegue a entender o word embeddings, precisamos entender como fazer operações algébricas com vetores.
1.2 Operações com Vetores
Dado um vetor $\vec{a}=(a_0,a_1,\ldots,a_{n-1})$ em $\mathbb{R}^n$, cada operação precisa dizer o que acontece com suas $n$ componentes. Também precisamos respeitar as dimensões: duas operações componente a componente só fazem sentido entre vetores do mesmo espaço $\mathbb{R}^n$. Reutilizaremos, no texto e no programa da Seção 1.3, os vetores $\vec{a}=(2,5,1)$ e $\vec{b}=(3,1,4)$.
-
Soma. Somar vetores significa somar componentes que ocupam a mesma posição:
\[\vec{a}+\vec{b}=(a_0+b_0,a_1+b_1,\ldots,a_{n-1}+b_{n-1}).\]No nosso exemplo, $\vec{a}+\vec{b}=(2+3,5+1,1+4)=(5,6,5)$.
-
Subtração. Subtrair $\vec{b}$ de $\vec{a}$ equivale a somar o oposto de $\vec{b}$; por isso, a operação também é componente a componente:
\[\vec{a}-\vec{b}=\vec{a}+(-\vec{b})=(a_0-b_0,a_1-b_1,\ldots,a_{n-1}-b_{n-1}).\]Assim, $\vec{a}-\vec{b}=(2-3,5-1,1-4)=(-1,4,-3)$.
-
Oposto. O oposto de $\vec{a}$ troca o sinal de cada componente. O resultado tem a mesma magnitude e a direção inversa:
\[-\vec{a}=(-a_0,-a_1,\ldots,-a_{n-1})=(-2,-5,-1).\] -
Multiplicação por escalar. Um escalar $\alpha$ multiplica todas as componentes do vetor:
\[\alpha\vec{a}=(\alpha a_0,\alpha a_1,\ldots,\alpha a_{n-1}).\]Para $\alpha=2$, obtemos $2\vec{a}=(4,10,2)$. Se $\alpha$ for negativo, além de alterar a magnitude, a multiplicação inverte a direção.
-
Produto escalar. Multiplicar dois vetores entre si, neste artigo, significará multiplicar componentes correspondentes e somar os produtos. O resultado não é outro vetor, mas um escalar:
\[\vec{a}\cdot\vec{b}=\sum_{i=0}^{n-1}a_i b_i.\]Para os vetores escolhidos, $\vec{a}\cdot\vec{b}=2\cdot3+5\cdot1+1\cdot4=15$. A Seção 2 retomará esta operação para deduzir sua interpretação geométrica e sua relação com similaridade.
-
Magnitude e normalização. A magnitude, ou norma euclidiana, mede o comprimento do vetor. Ela é a raiz quadrada do produto escalar do vetor consigo mesmo:
\[\lVert\vec{a}\rVert_2=\sqrt{\vec{a}\cdot\vec{a}} =\sqrt{\sum_{i=0}^{n-1}a_i^2}.\]No exemplo, $\lVert\vec{a}\rVert_2=\sqrt{2^2+5^2+1^2}=\sqrt{30}\approx5{,}4772$. Normalizar $\vec{a}$ significa dividir cada componente por essa magnitude para obter um vetor unitário $\hat{a}$, isto é, com magnitude $1$:
\[\hat{a}=\frac{\vec{a}}{\lVert\vec{a}\rVert_2} \approx(0{,}3651,0{,}9129,0{,}1826), \qquad \lVert\hat{a}\rVert_2=1.\]O vetor nulo não pode ser normalizado, pois sua magnitude é $0$ e a divisão por zero não é definida. Essa restrição explica a verificação feita pelo código antes de dividir.
1.3 Exemplo de Operações com Vetores em C++
O programa reúne exatamente as operações definidas na Seção 1.2. Os operadores +, - e * percorrem as componentes e aplicam as equações componente a componente; dot() acumula $\sum_i a_i b_i$; magnitude() calcula $\sqrt{\vec{a}\cdot\vec{a}}$; e normalize() divide cada componente pela magnitude depois de rejeitar o vetor nulo. Compile o exemplo com g++ -std=c++20 -O2 math_vector.cpp -o math_vector.
#include <iostream> /// Para entrada e saída padrão (std::cout, std::cerr).
#include <vector> /// Para contêiner std::vector usado no armazenamento de componentes.
#include <numeric> /// Para std::inner_product, usado no cálculo do produto escalar.
#include <cmath> /// Para std::sqrt e std::abs, usados em cálculos de magnitude.
#include <concepts> /// Para std::is_arithmetic_v, usado no conceito Arithmetic.
#include <type_traits> /// Para os predicados de tipos aritméticos usados pelo template.
#include <stdexcept> /// Para exceções padrão como std::out_of_range e std::invalid_argument.
#include <string> /// Para std::string e std::to_string, usados na conversão para string.
#include <sstream> /// Para std::stringstream, usado na formatação de números de ponto flutuante.
#include <iomanip> /// Para std::fixed e std::setprecision, usados na formatação de saída.
#include <initializer_list> /// Para a inicialização de vetores com listas inicializadoras.
#include <algorithm> /// Para std::abs, usado em cálculos de magnitude.
/**
* @concept Arithmetic
* @brief Conceito para garantir que um tipo é aritmético (integral ou de ponto flutuante).
*
* Restringe os parâmetros de template a tipos aritméticos (por exemplo, int, double, float).
*/
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
/**
* @class MathVector
* @brief Uma classe genérica para representar e manipular vetores matemáticos.
*
* Fornece operações como adição, subtração, multiplicação por escalar, produto escalar,
* cálculo de magnitude e normalização. Suporta vetores de qualquer tipo aritmético.
*
* @tparam T O tipo aritmético dos componentes do vetor (por exemplo, int, double).
*/
template<Arithmetic T>
class MathVector {
private:
std::vector<T> components; ///< Armazenamento interno para os componentes do vetor.
static constexpr T epsilon = 1e-9; ///< Constante pequena para comparações de ponto flutuante.
public:
/**
* @brief Construtor padrão que cria um vetor vazio.
*/
MathVector() = default;
/**
* @brief Constrói um vetor a partir de uma lista inicializadora.
* @param init Lista inicializadora contendo os componentes do vetor.
*/
MathVector(std::initializer_list<T> init) : components(init) {}
/**
* @brief Constrói um vetor a partir de um std::vector.
* @param vec O vetor de entrada contendo os componentes.
*/
explicit MathVector(const std::vector<T>& vec) : components(vec) {}
/**
* @brief Constrói um vetor de tamanho especificado com todos os componentes inicializados com um valor.
* @param n O número de componentes.
* @param val O valor inicial para todos os componentes (padrão é T{}).
*/
explicit MathVector(size_t n, T val = T{}) : components(n, val) {}
/**
* @brief Retorna o número de dimensões (componentes) do vetor.
* @return O tamanho do vetor.
*/
size_t dimensions() const {
return components.size();
}
/**
* @brief Fornece acesso não constante a um componente do vetor.
* @param index O índice do componente a acessar.
* @return Referência ao componente no índice especificado.
* @throws std::out_of_range Se o índice estiver fora dos limites.
*/
T& operator[](size_t index) {
if (index >= dimensions()) {
throw std::out_of_range("Índice fora dos limites do vetor");
}
return components[index];
}
/**
* @brief Fornece acesso constante a um componente do vetor.
* @param index O índice do componente a acessar.
* @return Referência constante ao componente no índice especificado.
* @throws std::out_of_range Se o índice estiver fora dos limites.
*/
const T& operator[](size_t index) const {
if (index >= dimensions()) {
throw std::out_of_range("Índice fora dos limites do vetor");
}
return components[index];
}
/**
* @brief Soma dois vetores componente a componente.
* @param other O vetor a ser somado a este vetor.
* @return Um novo vetor representando a soma.
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
MathVector<T> operator+(const MathVector<T>& other) const {
if (dimensions() != other.dimensions()) {
throw std::invalid_argument("Não é possível somar vetores de dimensões diferentes");
}
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = components[i] + other[i];
}
return result;
}
/**
* @brief Subtrai um vetor de outro componente a componente.
* @param other O vetor a ser subtraído deste vetor.
* @return Um novo vetor representando a diferença.
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
MathVector<T> operator-(const MathVector<T>& other) const {
if (dimensions() != other.dimensions()) {
throw std::invalid_argument("Não é possível subtrair vetores de dimensões diferentes");
}
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = components[i] - other[i];
}
return result;
}
/**
* @brief Multiplica o vetor por um escalar.
* @param scalar O valor escalar para multiplicação.
* @return Um novo vetor com componentes escalados.
*/
MathVector<T> operator*(T scalar) const {
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = components[i] * scalar;
}
return result;
}
/**
* @brief Retorna o vetor oposto (negado).
* @return Um novo vetor com todos os componentes negados.
*/
MathVector<T> operator-() const {
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = -components[i];
}
return result;
}
/**
* @brief Calcula o produto escalar deste vetor com outro.
* @param other O outro vetor para o produto escalar.
* @return O resultado escalar do produto escalar.
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
T dot(const MathVector<T>& other) const {
if (dimensions() != other.dimensions()) {
throw std::invalid_argument("Não é possível calcular o produto escalar de vetores de dimensões diferentes");
}
return std::inner_product(components.begin(), components.end(), other.components.begin(), T(0));
}
/**
* @brief Calcula a magnitude euclidiana (norma L2) do vetor.
* @return A magnitude do vetor.
* @note Para tipos integrais, converte para double para sqrt e converte de volta.
*/
T magnitude() const {
T sum_of_squares = this->dot(*this);
if constexpr (std::is_integral_v<T>) {
return static_cast<T>(std::sqrt(static_cast<double>(sum_of_squares)));
} else {
return std::sqrt(sum_of_squares);
}
}
/**
* @brief Normaliza o vetor para ter comprimento unitário.
* @return Um novo vetor normalizado.
* @throws std::domain_error Se a magnitude do vetor for zero ou próxima de zero.
*/
MathVector<T> normalize() const {
T mag = magnitude();
if (std::abs(mag) < epsilon) {
throw std::domain_error("Não é possível normalizar um vetor de magnitude zero (ou muito próxima de zero)");
}
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = components[i] / mag;
}
return result;
}
/**
* @brief Converte o vetor para uma representação em string.
* @return Uma string representando o vetor (por exemplo, "[1.0000, 2.0000]").
* @note Para tipos de ponto flutuante, usa precisão fixa de 4 casas decimais.
*/
std::string to_string() const {
std::string result = "[";
for (size_t i = 0; i < dimensions(); ++i) {
if constexpr (std::is_floating_point_v<T>) {
std::stringstream ss;
ss << std::fixed << std::setprecision(4) << components[i];
result += ss.str();
} else {
result += std::to_string(components[i]);
}
if (i < dimensions() - 1) {
result += ", ";
}
}
result += "]";
return result;
}
/**
* @brief Fornece iterador para o início dos componentes do vetor.
* @return Iterador para o primeiro componente.
*/
auto begin() const {
return components.begin();
}
/**
* @brief Fornece iterador para o fim dos componentes do vetor.
* @return Iterador após o último componente.
*/
auto end() const {
return components.end();
}
/**
* @brief Fornece iterador não constante para o início dos componentes do vetor.
* @return Iterador para o primeiro componente.
*/
auto begin() {
return components.begin();
}
/**
* @brief Fornece iterador não constante para o fim dos componentes do vetor.
* @return Iterador após o último componente.
*/
auto end() {
return components.end();
}
/**
* @brief Retorna o vetor de componentes subjacente.
* @return Referência constante ao std::vector interno de componentes.
*/
const std::vector<T>& get_components() const {
return components;
}
};
/**
* @brief Multiplica um escalar por um vetor (escalar * vetor).
* @tparam T O tipo aritmético dos componentes do vetor.
* @param scalar O valor escalar.
* @param vec O vetor a ser multiplicado.
* @return Um novo vetor com componentes escalados.
*/
template<Arithmetic T>
MathVector<T> operator*(T scalar, const MathVector<T>& vec) {
return vec * scalar;
}
/**
* @brief Imprime um MathVector com um nome especificado.
* @tparam T O tipo aritmético dos componentes do vetor.
* @param vec O vetor a ser impresso.
* @param name O nome a ser exibido ao lado do vetor.
*/
template<Arithmetic T>
void print_mathvector(const MathVector<T>& vec, const std::string& name) {
std::cout << name << " = " << vec.to_string() << "\n";
}
/**
* @brief Função principal que demonstra operações com a classe MathVector.
*
* Este programa ilustra o uso da classe MathVector para realizar operações como adição,
* subtração, multiplicação por escalar, cálculo do vetor oposto, produto escalar,
* magnitude e normalização de vetores, com tratamento de erros robusto.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(4);
std::cout << "Demonstração de Operações com MathVector\n";
std::cout << "---------------------------------------\n\n";
// Inicialização dos vetores
MathVector<double> a = {2.0, 5.0, 1.0}; ///< Primeiro vetor para demonstração.
MathVector<double> b = {3.0, 1.0, 4.0}; ///< Segundo vetor para demonstração.
// Exemplo 1: Operações básicas
std::cout << "Exemplo 1: Operações básicas\n";
try {
print_mathvector(a, "Vetor a");
print_mathvector(b, "Vetor b");
// Adição
MathVector<double> sum = a + b;
print_mathvector(sum, "a + b");
// Subtração
MathVector<double> diff = a - b;
print_mathvector(diff, "a - b");
// Multiplicação por escalar
MathVector<double> scaled = 2.0 * a;
print_mathvector(scaled, "2 * a");
// Vetor oposto
MathVector<double> opposite = -a;
print_mathvector(opposite, "-a");
} catch (const std::exception& e) {
std::cerr << "Erro nas operações básicas: " << e.what() << "\n";
}
// Exemplo 2: Produto escalar
std::cout << "\nExemplo 2: Produto escalar\n";
try {
double dot_product = a.dot(b);
std::cout << "a · b = " << dot_product << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro no cálculo do produto escalar: " << e.what() << "\n";
}
// Exemplo 3: Magnitude e normalização
std::cout << "\nExemplo 3: Magnitude e normalização\n";
try {
double mag_a = a.magnitude();
std::cout << "Magnitude de a = " << mag_a << "\n";
MathVector<double> a_normalized = a.normalize();
print_mathvector(a_normalized, "Vetor a normalizado");
std::cout << "Magnitude de a normalizado = " << a_normalized.magnitude() << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro no cálculo de magnitude ou normalização: " << e.what() << "\n";
}
return 0;
}
2. Produto Escalar
Entre as operações entre vetores vamos começar com os produtos escalares, também conhecidos como produto interno. Neste caso, temos uma técnica para multiplicar vetores de forma que o resultado seja um escalar, um número sem dimensão. Para obter o produto escalar, representado por $\cdot$, de dois vetores, multiplicamos seus elementos correspondentes e, em seguida, somamos os resultados das multiplicações. Matematicamente temos:
\[\text{Se } \vec{a} = [a_1, a_2, ..., a_n] \text{ e } \vec{b} = [b_1, b_2, ..., b_n], \text{ então:}\] \[\vec{a} \cdot \vec{b} = \sum_{i=1}^{n} a_i b_i = a_1b_1 + a_2b_2 + ... + a_nb_n\]Exemplo 1: Considerando os vetores $\vec{a} = [2, 5, 1]$ e $\vec{b} = [3, 1, 4]$. O produto escalar será dado por:
\[\vec{a} \cdot \vec{b} = (2 \times 3) + (5 \times 1) + (1 \times 4) = 6 + 5 + 4 = 15\]Melhor do que acompanhar uma conta pronta é refazê-la: no laboratório abaixo, a atenta leitora pode editar cada uma das três componentes dos vetores $\vec{a}$ e $\vec{b}$ e ver a mecânica completa do produto escalar, os produtos componente a componente e a soma final, recalculada a cada digitação. Comece conferindo o Exemplo 1, que já vem carregado, e depois experimente: o que acontece se uma componente for negativa? E se os produtos se cancelarem exatamente?
O produto escalar também pode ser representado na forma matricial. Se considerarmos os vetores como matrizes, o produto escalar será obtido multiplicando a transposta do primeiro vetor pelo segundo vetor:
\[\vec{a} \cdot \vec{b} = \vec{a}^\top\vec{b} = \begin{bmatrix} a_1 & a_2 & \cdots & a_n \end{bmatrix} \begin{bmatrix} b_1 \\ b_2 \\ \vdots \\ b_n \end{bmatrix} = \sum_{i=1}^{n} a_i b_i\]A transposta de um vetor é uma operação da álgebra linear que altera a orientação do vetor, convertendo um vetor coluna em um vetor linha ou vice-versa. Formalmente:
Se $\vec{v}$ é um vetor coluna $\vec{v} = \begin{pmatrix} v_1 \ v_2 \ \vdots \ v_n \end{pmatrix}$, sua transposta $\vec{v}^\top$ é um vetor linha $\vec{v}^\top = \begin{pmatrix} v_1 & v_2 & \cdots & v_n \end{pmatrix}$
Se $\vec{v}$ é um vetor linha $\vec{v} = \begin{pmatrix} v_1 & v_2 & \cdots & v_n \end{pmatrix}$, sua transposta $\vec{v}^\top$ é um vetor coluna $\vec{v}^\top = \begin{pmatrix} v_1 \ v_2 \ \vdots \ v_n \end{pmatrix}$
Em notação matricial, se representarmos um vetor coluna $\vec{v} \in \mathbb{R}^n$ como uma matriz $n \times 1$, sua transposta $\vec{v}^\top$ será uma matriz $1 \times n$. A operação de transposição é indicada pelo sobrescrito $\top$.
Em sistemas de processamento de linguagem natural, a transposição de vetores é frequentemente utilizada em operações de atenção e em cálculos de similaridade entre vetores de embedding.
Para ilustrar, considere os vetores $\vec{a} = [2, 5, 1]$ e $\vec{b} = [3, 1, 4]$. Na forma matricial, temos:
\[\vec{a}^\top\vec{b} = \begin{bmatrix} 2 & 5 & 1 \end{bmatrix} \begin{bmatrix} 3 \\ 1 \\ 4 \end{bmatrix} = (2 \times 3) + (5 \times 1) + (1 \times 4) = 15\]2.1 Produto Escalar com Bibliotecas Numéricas
Em Python podemos usar o JAX, uma biblioteca para computação de alta performance com suporte para diferenciação automática, o produto escalar pode ser calculado usando a função jax.numpy.dot() ou o operador @.
import jax.numpy as jnp
# Defina dois vetores
v1 = jnp.array([1, 2, 3])
v2 = jnp.array([4, 5, 6])
# Calcule o produto escalar usando dot()
dot_product1 = jnp.dot(v1, v2)
# Calcule o produto escalar usando o operador @
dot_product2 = v1 @ v2
print(f"Produto escalar usando dot(): {dot_product1}")
print(f"Produto escalar usando @: {dot_product2}")
Em C++ podemos usar o Eigen, uma biblioteca de álgebra linear de alto desempenho, o produto escalar pode ser calculado usando o método dot() ou o operador de multiplicação de matrizes.
#include <iostream>
#include <Eigen/Dense>
/**
* @brief Função principal que demonstra o cálculo do produto escalar usando a biblioteca Eigen.
*
* Este programa define dois vetores tridimensionais e calcula seu produto escalar de duas formas:
* utilizando o método `dot()` da biblioteca Eigen e utilizando a multiplicação matricial com a transposta
* de um dos vetores. Os resultados são exibidos no console.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
// Defina dois vetores tridimensionais
Eigen::Vector3d v1(1, 2, 3);
Eigen::Vector3d v2(4, 5, 6);
// Calcula o produto escalar usando o método dot()
double dot_product1 = v1.dot(v2);
// Calcula o produto escalar usando a transposta e multiplicação matricial
double dot_product2 = v1.transpose() * v2;
std::cout << "Produto escalar usando dot(): " << dot_product1 << std::endl;
std::cout << "Produto escalar usando transpose e multiplicação: " << dot_product2 << std::endl;
return 0;
}
2.2 Exemplo em C++ de Produto Escalar
O programa abaixo traduz diretamente a definição da Seção 2: std::inner_product percorre os dois vetores em paralelo, multiplica cada par de componentes e acumula os produtos a partir de zero. std::span fornece à função apenas uma visão dos dados contíguos, sem copiá-los. Como o emparelhamento exige uma componente de $\vec{b}$ para cada componente de $\vec{a}$, dimensões diferentes produzem uma exceção. Compile o exemplo com g++ -std=c++20 -O2 dot_product.cpp -o dot_product.
#include <iostream>
#include <numeric>
#include <span>
#include <stdexcept>
#include <type_traits>
#include <vector>
template <typename T>
requires std::is_arithmetic_v<T>
T dot(std::span<const T> a, std::span<const T> b) {
if (a.size() != b.size()) {
throw std::invalid_argument(
"O produto escalar exige vetores com a mesma dimensão");
}
// A soma começa no zero do próprio tipo T e acumula a_i * b_i.
return std::inner_product(a.begin(), a.end(), b.begin(), T{});
}
int main() {
const std::vector<double> a{2.0, 5.0, 1.0};
const std::vector<double> b{3.0, 1.0, 4.0};
try {
std::cout << "a · b = " << dot<double>(a, b) << '\n';
} catch (const std::invalid_argument& error) {
std::cerr << "Erro: " << error.what() << '\n';
return 1;
}
}
A saída é a · b = 15, exatamente o valor calculado na abertura da Seção 2. O código não precisa conhecer magnitude, normalização nem multiplicação genérica de matrizes para demonstrar esse mecanismo; essas operações só aparecerão depois de suas definições.
2.3 O Produto Escalar e a Similaridade
O produto escalar oferece uma medida quantitativa da similaridade direcional entre dois vetores. Embora não constitua uma métrica de similaridade completa em todos os contextos, fornece informações valiosas sobre o alinhamento vetorial. Em termos gerais, a interpretação do produto escalar $\vec{u} \cdot \vec{v}$ segue estas propriedades:
\[\vec{u} \cdot \vec{v} = \vert \vec{u} \vert \vert \vec{v} \vert \cdot \cos(\theta)\]Na qual $\theta$ representa o ângulo entre os vetores. Podemos observar que:
- $\vec{u} \cdot \vec{v} > 0$: Os vetores apontam em direções geralmente similares (ângulo agudo). Quanto maior o valor positivo, maior a similaridade em termos de direção e magnitude das componentes que se alinham.
- $\vec{u} \cdot \vec{v} = 0$: Os vetores são ortogonais (perpendiculares). Não há similaridade direcional linear entre eles.
- $\vec{u} \cdot \vec{v} < 0$: Os vetores apontam em direções geralmente opostas (ângulo obtuso). Quanto mais negativo, maior a dissimilaridade direcional.

Figura 1: Os três regimes do produto escalar: vetores em direções similares, ortogonais e opostas.
Para vetores normalizados (de magnitude unitária), o produto escalar se reduz diretamente ao cosseno do ângulo entre eles, fornecendo uma medida de similaridade no intervalo $[-1, 1]$, frequentemente utilizada em sistemas de recuperação de informação e processamento de linguagem natural.
Exemplo 2: considerando os vetores $\vec{a} = [0, 1, 0]$ e $\vec{b} = [0.2, 0.7, 0.1]$, o produto escalar será:
\[\vec{a} \cdot \vec{b} = (0 \times 0.2) + (1 \times 0.7) + (0 \times 0.1)\] \[\vec{a} \cdot \vec{b} = 0 + 0.7 + 0 = 0.7\]No exemplo 2, o vetor $\vec{a} = [0, 1, 0]$ pode ser visto como um vetor que ativa, ou dá peso máximo, apenas à segunda dimensão, e peso zero às demais. Ao calcular o produto escalar com $\vec{b} = [0.2, 0.7, 0.1]$, estamos essencialmente extraindo ou medindo o valor da segunda componente de $b$ (que é $0.7$), ponderado pela importância, ou peso que o vetor $a$ atribui a essa dimensão.
Com um pouco mais de formalidade: se temos dois vetores $\vec{u}$ e $\vec{v}$, e você calcula $\vec{u} \cdot \vec{v} = c$, o valor escalar $c$ pode ser interpretado como uma medida de:
- Quanto de $\vec{v}$ “existe” na direção de $\vec{u}$ (e vice-versa);
- O grau de alinhamento ou sobreposição entre os vetores;
- A similaridade entre os padrões representados pelos vetores, no sentido de que componentes importantes em um vetor também são relevantes no outro, com pesos proporcionais aos valores das componentes.
A criativa leitora deve notar que o produto escalar é influenciado tanto pela direção quanto pela magnitude dos vetores.
A magnitude de um vetor é dada pela raiz quadrada da soma dos quadrados dos seus componentes. Isso é equivalente a tirar a raiz quadrada do resultado do produto escalar do vetor com ele mesmo. Para um vetor
\[\vec{v} = \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix}\]em um espaço $n$-dimensional, a magnitude, eventualmente chamada de Norma Euclidiana, representada por $ \vert \vec{v} \vert $ será definida por:
\[\vert \vec{v} \vert = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2} = \sqrt{\sum_{i=1}^{n} v_i^2}\]
Exemplo 3: dado o vetor $\vec{b} = \begin{bmatrix} 0.2 \ 0.7 \ 0.1 \end{bmatrix}$, vamos calcular sua magnitude $ \vert \vec{b} \vert $:
Podemos resolver este problema em dois passos:
-
Calcular o produto escalar de $\vec{b}$ consigo mesmo:
\[\vec{b} \cdot \vec{b} = (0.2 \times 0.2) + (0.7 \times 0.7) + (0.1 \times 0.1)\] \[\vec{b} \cdot \vec{b} = 0.04 + 0.49 + 0.01 = 0.54\] -
Extrair a raiz quadrada do resultado:
\[\vert \vec{b} \vert = \sqrt{\vec{b} \cdot \vec{b}} = \sqrt{0.54} \approx 0.7348\]
Portanto, a magnitude do vetor $\vec{b} = \begin{bmatrix} 0.2 \ 0.7 \ 0.1 \end{bmatrix}$ é aproximadamente 0.7348.
A magnitude pode ter interpretações diferentes em áreas diferentes do conhecimento. Na física, pode representar a intensidade de uma força, ou uma velocidade. No estudo da linguagem natural, a magnitude de um vetor, pode indicar o tamanho do documento em termos de número de palavras, embora não diretamente.
A atenta leitora deve observar que vetores com magnitudes maiores tendem a ter produtos escalares maiores, mesmo que a direção relativa seja a mesma.
Com as definições de produto escalar ($\vec{u} \cdot \vec{v}$) e magnitude ($ \vert \vec{u} \vert $, $ \vert \vec{v} \vert $) em mãos, podemos reorganizar a relação fundamental $\vec{u} \cdot \vec{v} = \vert \vec{u} \vert \cdot \vert \vec{v} \vert \cdot \cos(\theta)$ para isolar o cosseno do ângulo $\theta$. Isso nos fornece diretamente a fórmula da Similaridade de Cosseno, uma das métricas mais importantes em processamento de linguagem natural e recuperação de informação para medir a similaridade direcional entre dois vetores:
\[\text{Similaridade de Cosseno}(\vec{u}, \vec{v}) = \cos(\theta) = \frac{\vec{u} \cdot \vec{v}}{ \vert \vec{u} \vert \vert \vec{v} \vert }\]Esta métrica varia no intervalo $[-1, 1]$:
- $1$: Indica que os vetores apontam exatamente na mesma direção (ângulo $0^\circ$).
- $0$: Indica que os vetores são ortogonais (perpendiculares, ângulo $90^\circ$).
- $-1$: Indica que os vetores apontam em direções exatamente opostas (ângulo $180^\circ$).
A grande vantagem da Similaridade de Cosseno é que ela foca puramente na direção dos vetores, ignorando suas magnitudes. Isso é particularmente útil ao comparar documentos de tamanhos diferentes ou word embeddings, nos quais a orientação no espaço semântico é mais importante que a magnitude absoluta do vetor.
Quando estudamos processamento de linguagem natural, a magnitude por si não costuma ser a informação mais importante, ou mais buscada. Geralmente estamos interessados na direção de vetores e na similaridade entre eles.
A similaridade entre vetores é uma medida de quão semelhantes são dois vetores em termos de direção e magnitude. Essa medida é fundamental em muitas aplicações, como recuperação de informação, recomendação de produtos e análise de sentimentos.
Em alguns casos, a busca da similaridade implica na normalização dos vetores para que a medida de similaridade seja mais afetada pela direção e menos afetada pela magnitude.
A normalização de um vetor $\vec{v}$ consiste em dividi-lo por sua norma (ou magnitude), resultando em um vetor unitário $\hat{v}$ que mantém a mesma direção, mas possui comprimento 1:
\[\hat{v} = \frac{\vec{v}}{ \vert \vec{v} \vert } = \frac{\vec{v}}{\sqrt{v_1^2 + v_2^2 + \cdots + v_n^2}}\]Neste caso, $ \vert \vec{v} \vert $ representa a norma euclidiana do vetor. Quando dois vetores normalizados são comparados através do produto escalar, o resultado varia apenas entre $-1$ e $1$, correspondendo diretamente ao cosseno do ângulo entre eles. Esta abordagem é particularmente útil em aplicações como recuperação de informações, sistemas de recomendação e processamento de linguagem natural, onde a orientação semântica dos vetores é geralmente mais relevante que suas magnitudes absolutas.
Para ajudar a visualizar esse conceito a amável leitora pode usar o explorador interativo abaixo. Experimente alterar as coordenadas dos vetores $\vec{A}$ e $\vec{B}$ e observe como o ângulo e a similaridade de cosseno respondem em tempo real.
A norma euclidiana, também conhecida como norma $L_2$ ou comprimento euclidiano, é uma função que atribui a cada vetor um valor escalar não-negativo que pode ser interpretado como o “tamanho” ou “magnitude” do vetor. Para um vetor $\vec{v} = (v_1, v_2, \ldots, v_n)$ em $\mathbb{R}^n$, a norma euclidiana é definida como:
\[\vert \vec{v} \vert = \sqrt{v_1^2 + v_2^2 + \cdots + v_n^2} = \sqrt{\sum_{i=1}^{n} v_i^2}\]Esta definição é uma generalização do teorema de Pitágoras para espaços de dimensão arbitrária. Geometricamente, representa a distância do ponto representado pelo vetor à origem do sistema de coordenadas.
A norma euclidiana possui as seguintes propriedades fundamentais:
- Não-negatividade: $ \vert \vec{v} \vert \geq 0$ para todo $\vec{v}$, e $ \vert \vec{v} \vert = 0$ se e somente se $\vec{v} = \vec{0}$
- Homogeneidade: $ \vert \alpha\vec{v} \vert = \vert \alpha \vert \cdot \vert \vec{v} \vert $ para qualquer escalar $\alpha$
- Desigualdade triangular: $ \vert \vec{u} + \vec{v} \vert \leq \vert \vec{u} \vert + \vert \vec{v} \vert $
Estas propriedades fazem da norma euclidiana uma ferramenta essencial em diversos campos, desde geometria e física até aprendizado de máquina e processamento de sinais, onde é utilizada para medir distâncias, calcular erros, e normalizar vetores.
Técnicas como a Similaridade de Cosseno, que envolve o produto escalar normalizado pelas magnitudes do vetores, são usadas para isolar a similaridade direcional. Todavia, existem diversas técnicas diferentes para a determinação de um índice para a similaridade entre vetores. Entre elas destacaremos:
-
Distância Euclidiana: mede a distância “direta” entre dois pontos no espaço euclidiano, $\sqrt{\sum_i (u_i - v_i)^2}$. É sensível tanto à direção quanto à magnitude: dois documentos sobre o mesmo assunto, um curto e um longo, podem terminar distantes um do outro;
-
Distância de Manhattan: também conhecida como distância $L_1$, mede a soma das diferenças absolutas das coordenadas, $\sum_i \vert u_i - v_i \vert$, como quem percorre as ruas de uma cidade em malha quadriculada em vez de atravessar os quarteirões em linha reta;
-
Distância de Minkowski: uma generalização das duas anteriores, $\left(\sum_i \vert u_i - v_i \vert^p\right)^{1/p}$. Com $p=2$ recuperamos a distância euclidiana; com $p=1$, a de Manhattan;
-
Similaridade de Jaccard: usada principalmente para conjuntos, mede a razão entre o tamanho da interseção e o tamanho da união. Para dois documentos vistos como conjuntos de palavras, quantifica o vocabulário compartilhado;
-
Correlação de Pearson: mede a correlação linear entre dois vetores, variando de $-1$ a $1$. Equivale à similaridade de cosseno calculada depois de subtrair de cada vetor a sua média;
-
Distância de Mahalanobis: considera a correlação entre as variáveis, e é útil quando as componentes dos vetores têm escalas ou distribuições diferentes.
Algumas já foram usadas em processamento de linguagem natural. Outras ainda não. Vamos trabalhar com cada uma delas se, e quando, forem usadas nos algoritmos que estudaremos. A primeira que usaremos será a Similaridade de Cosseno. Mas antes, precisamos entender como representar textos como vetores. Para isso, vamos começar com a representação mais simples e intuitiva: a Frequência de Termos.
2.4 Exemplo de Produto Escalar em C++
#include <iostream> ///< Para entrada e saída padrão (std::cout, std::cerr).
#include <vector> ///< Para contêiner std::vector usado no armazenamento de componentes.
#include <numeric> ///< Para std::inner_product, usado no cálculo do produto escalar.
#include <cmath> ///< Para std::sqrt e std::abs, usados em cálculos de magnitude e distâncias.
#include <concepts> ///< Para std::is_arithmetic_v, usado no conceito Arithmetic.
#include <stdexcept> ///< Para exceções padrão como std::out_of_range e std::invalid_argument.
#include <string> ///< Para std::string e std::to_string, usados na conversão para string.
#include <sstream> ///< Para std::stringstream, usado na formatação de números de ponto flutuante.
#include <iomanip> ///< Para std::fixed e std::setprecision, usados na formatação de saída.
#include <initializer_list> ///< Para a inicialização de vetores com listas inicializadoras.
#include <algorithm> ///< Para std::abs, usado no cálculo da distância de Manhattan.
/**
* @concept Arithmetic
* @brief Conceito para garantir que um tipo é aritmético (integral ou de ponto flutuante).
*
* Restringe os parâmetros de template a tipos aritméticos (por exemplo, int, double, float).
*/
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
/**
* @class MathVector
* @brief Uma classe genérica para representar e manipular vetores matemáticos.
*
* Suporta operações como produto escalar, cálculo de magnitude, normalização e conversão para string.
* Usada como base para cálculos de similaridade entre vetores.
*
* @tparam T O tipo aritmético dos componentes do vetor (por exemplo, int, double).
*/
template<Arithmetic T>
class MathVector {
private:
std::vector<T> components; ///< Armazenamento interno para os componentes do vetor.
static constexpr T epsilon = 1e-9; ///< Constante pequena para comparações de ponto flutuante.
public:
/**
* @brief Construtor padrão que cria um vetor vazio.
*/
MathVector() = default;
/**
* @brief Constrói um vetor a partir de uma lista inicializadora.
* @param init Lista inicializadora contendo os componentes do vetor.
*/
MathVector(std::initializer_list<T> init) : components(init) {}
/**
* @brief Constrói um vetor a partir de um std::vector.
* @param vec O vetor de entrada contendo os componentes.
*/
explicit MathVector(const std::vector<T>& vec) : components(vec) {}
/**
* @brief Constrói um vetor de tamanho especificado com todos os componentes inicializados com um valor.
* @param n O número de componentes.
* @param val O valor inicial para todos os componentes (padrão é T{}).
*/
explicit MathVector(size_t n, T val = T{}) : components(n, val) {}
/**
* @brief Retorna o número de dimensões (componentes) do vetor.
* @return O tamanho do vetor.
*/
size_t dimensions() const {
return components.size();
}
/**
* @brief Fornece acesso não constante a um componente do vetor.
* @param index O índice do componente a acessar.
* @return Referência ao componente no índice especificado.
* @throws std::out_of_range Se o índice estiver fora dos limites.
*/
T& operator[](size_t index) {
if (index >= dimensions()) {
throw std::out_of_range("Índice fora dos limites do vetor");
}
return components[index];
}
/**
* @brief Fornece acesso constante a um componente do vetor.
* @param index O índice do componente a acessar.
* @return Referência constante ao componente no índice especificado.
* @throws std::out_of_range Se o índice estiver fora dos limites.
*/
const T& operator[](size_t index) const {
if (index >= dimensions()) {
throw std::out_of_range("Índice fora dos limites do vetor");
}
return components[index];
}
/**
* @brief Calcula o produto escalar deste vetor com outro.
* @param other O outro vetor para o produto escalar.
* @return O resultado escalar do produto escalar.
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
T dot(const MathVector<T>& other) const {
if (dimensions() != other.dimensions()) {
throw std::invalid_argument("Dimensões diferentes para produto escalar");
}
return std::inner_product(components.begin(), components.end(), other.components.begin(), T(0));
}
/**
* @brief Calcula a magnitude euclidiana (norma L2) do vetor.
* @return A magnitude do vetor.
* @note Para tipos integrais, converte para double para sqrt e converte de volta.
*/
T magnitude() const {
T sum_of_squares = this->dot(*this);
if constexpr (std::is_integral_v<T>) {
return static_cast<T>(std::sqrt(static_cast<double>(sum_of_squares)));
} else {
return std::sqrt(sum_of_squares);
}
}
/**
* @brief Normaliza o vetor para ter comprimento unitário.
* @return Um novo vetor normalizado.
* @throws std::domain_error Se a magnitude do vetor for zero ou próxima de zero.
*/
MathVector<T> normalize() const {
T mag = magnitude();
if (std::abs(mag) < epsilon) {
throw std::domain_error("Não é possível normalizar um vetor de magnitude zero (ou muito próxima de zero)");
}
MathVector<T> result(dimensions());
for (size_t i = 0; i < dimensions(); ++i) {
result[i] = components[i] / mag;
}
return result;
}
/**
* @brief Converte o vetor para uma representação em string.
* @return Uma string representando o vetor (por exemplo, "[1.0000, 2.0000]").
* @note Para tipos de ponto flutuante, usa precisão fixa de 4 casas decimais.
*/
std::string to_string() const {
std::string result = "[";
for (size_t i = 0; i < dimensions(); ++i) {
if constexpr (std::is_floating_point_v<T>) {
std::stringstream ss;
ss << std::fixed << std::setprecision(4) << components[i];
result += ss.str();
} else {
result += std::to_string(components[i]);
}
if (i < dimensions() - 1) {
result += ", ";
}
}
result += "]";
return result;
}
};
/**
* @class VectorSimilarity
* @brief Uma classe para calcular métricas de similaridade e distância entre vetores.
*
* Fornece métodos para produto escalar, similaridade de cosseno, distâncias euclidiana e de Manhattan,
* e normalização de vetores, com verificações robustas de dimensões e magnitude.
*
* @tparam T O tipo aritmético dos componentes do vetor (por exemplo, int, double).
*/
template<Arithmetic T>
class VectorSimilarity {
private:
static constexpr T epsilon = 1e-9; ///< Constante pequena para comparações de ponto flutuante.
/**
* @brief Verifica se dois vetores têm a mesma dimensão.
* @param a Primeiro vetor.
* @param b Segundo vetor.
* @return true se as dimensões forem iguais, false caso contrário.
*/
static bool check_dimensions(const MathVector<T>& a, const MathVector<T>& b) {
return a.dimensions() == b.dimensions();
}
public:
/**
* @brief Calcula o produto escalar entre dois vetores.
* @param a Primeiro vetor.
* @param b Segundo vetor.
* @return O resultado escalar do produto escalar.
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
static T dot_product(const MathVector<T>& a, const MathVector<T>& b) {
return a.dot(b);
}
/**
* @brief Calcula a similaridade de cosseno entre dois vetores.
* @param a Primeiro vetor.
* @param b Segundo vetor.
* @return O valor da similaridade de cosseno, no intervalo [-1, 1].
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
* @throws std::domain_error Se a magnitude de qualquer vetor for zero ou próxima de zero.
*/
static T cosine_similarity(const MathVector<T>& a, const MathVector<T>& b) {
if (!check_dimensions(a, b)) {
throw std::invalid_argument("Vetores com dimensões diferentes para similaridade de cosseno");
}
T mag_a = a.magnitude();
T mag_b = b.magnitude();
if (std::abs(mag_a) < epsilon || std::abs(mag_b) < epsilon) {
throw std::domain_error("Magnitude zero (ou próxima) em cálculo de similaridade de cosseno");
}
T dot = a.dot(b);
return dot / (mag_a * mag_b);
}
/**
* @brief Calcula a distância euclidiana entre dois vetores.
* @param a Primeiro vetor.
* @param b Segundo vetor.
* @return A distância euclidiana (norma L2 da diferença).
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
static T euclidean_distance(const MathVector<T>& a, const MathVector<T>& b) {
if (!check_dimensions(a, b)) {
throw std::invalid_argument("Vetores com dimensões diferentes para distância euclidiana");
}
T sum_of_squares = 0;
for (size_t i = 0; i < a.dimensions(); ++i) {
T diff = a[i] - b[i];
sum_of_squares += diff * diff;
}
if constexpr (std::is_integral_v<T>) {
return static_cast<T>(std::sqrt(static_cast<double>(sum_of_squares)));
} else {
return std::sqrt(sum_of_squares);
}
}
/**
* @brief Calcula a distância de Manhattan (norma L1) entre dois vetores.
* @param a Primeiro vetor.
* @param b Segundo vetor.
* @return A distância de Manhattan (soma das diferenças absolutas).
* @throws std::invalid_argument Se os vetores tiverem dimensões diferentes.
*/
static T manhattan_distance(const MathVector<T>& a, const MathVector<T>& b) {
if (!check_dimensions(a, b)) {
throw std::invalid_argument("Vetores com dimensões diferentes para distância de Manhattan");
}
T sum_of_abs_diff = 0;
for (size_t i = 0; i < a.dimensions(); ++i) {
sum_of_abs_diff += std::abs(a[i] - b[i]);
}
return sum_of_abs_diff;
}
/**
* @brief Normaliza um vetor para ter magnitude unitária.
* @param vec O vetor a ser normalizado.
* @return Um novo vetor normalizado.
* @throws std::domain_error Se a magnitude do vetor for zero ou próxima de zero.
*/
static MathVector<T> normalize(const MathVector<T>& vec) {
return vec.normalize();
}
};
/**
* @brief Imprime um MathVector com um nome especificado.
* @tparam T O tipo aritmético dos componentes do vetor.
* @param vec O vetor a ser impresso.
* @param name O nome a ser exibido ao lado do vetor.
*/
template<Arithmetic T>
void print_mathvector(const MathVector<T>& vec, const std::string& name) {
std::cout << name << " = " << vec.to_string() << "\n";
}
/**
* @brief Função principal que demonstra cálculos de similaridade e distância entre vetores.
*
* Este programa ilustra o uso da classe VectorSimilarity para calcular o produto escalar,
* similaridade de cosseno, distâncias euclidiana e de Manhattan, e normalização de vetores.
* Inclui exemplos com vetores alinhados, ortogonais, opostos, extração de componentes,
* e normalização, com tratamento de erros robusto.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(4);
std::cout << "Demonstração de Cálculos de Similaridade e Distância entre Vetores\n";
std::cout << "-----------------------------------------------------------------\n\n";
// Exemplo 1: Vetores alinhados (similares)
std::cout << "Exemplo 1: Vetores geralmente alinhados (similares)\n";
MathVector<double> u1 = {0.5, 0.8, 0.3}; ///< Primeiro vetor similar.
MathVector<double> v1 = {0.6, 0.9, 0.2}; ///< Segundo vetor similar.
try {
print_mathvector(u1, "Vetor u1");
print_mathvector(v1, "Vetor v1");
std::cout << "Produto escalar: " << VectorSimilarity<double>::dot_product(u1, v1) << "\n";
std::cout << "Similaridade de cosseno: " << VectorSimilarity<double>::cosine_similarity(u1, v1) << "\n";
std::cout << "Distância euclidiana: " << VectorSimilarity<double>::euclidean_distance(u1, v1) << "\n";
std::cout << "Distância de Manhattan: " << VectorSimilarity<double>::manhattan_distance(u1, v1) << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 2: Vetores ortogonais (perpendiculares)
std::cout << "\nExemplo 2: Vetores ortogonais (perpendiculares)\n";
MathVector<double> u2 = {1.0, 0.0, 0.0}; ///< Primeiro vetor ortogonal.
MathVector<double> v2 = {0.0, 1.0, 0.0}; ///< Segundo vetor ortogonal.
try {
print_mathvector(u2, "Vetor u2");
print_mathvector(v2, "Vetor v2");
std::cout << "Produto escalar: " << VectorSimilarity<double>::dot_product(u2, v2) << "\n";
std::cout << "Similaridade de cosseno: " << VectorSimilarity<double>::cosine_similarity(u2, v2) << "\n";
std::cout << "Distância euclidiana: " << VectorSimilarity<double>::euclidean_distance(u2, v2) << "\n";
std::cout << "Distância de Manhattan: " << VectorSimilarity<double>::manhattan_distance(u2, v2) << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 3: Vetores em direções opostas
std::cout << "\nExemplo 3: Vetores em direções opostas\n";
MathVector<double> u3 = {0.7, 0.2, -0.3}; ///< Primeiro vetor oposto.
MathVector<double> v3 = {-0.7, -0.2, 0.3}; ///< Segundo vetor oposto.
try {
print_mathvector(u3, "Vetor u3");
print_mathvector(v3, "Vetor v3");
std::cout << "Produto escalar: " << VectorSimilarity<double>::dot_product(u3, v3) << "\n";
std::cout << "Similaridade de cosseno: " << VectorSimilarity<double>::cosine_similarity(u3, v3) << "\n";
std::cout << "Distância euclidiana: " << VectorSimilarity<double>::euclidean_distance(u3, v3) << "\n";
std::cout << "Distância de Manhattan: " << VectorSimilarity<double>::manhattan_distance(u3, v3) << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 4: Extração de componente usando produto escalar
std::cout << "\nExemplo 4: Extração de componente usando produto escalar\n";
MathVector<double> base = {0.0, 1.0, 0.0}; ///< Vetor base para extração da segunda dimensão.
MathVector<double> data = {0.2, 0.7, 0.1}; ///< Vetor de dados para extração.
try {
print_mathvector(base, "Vetor base");
print_mathvector(data, "Vetor data");
double extracted = VectorSimilarity<double>::dot_product(base, data);
std::cout << "Produto escalar (extrai valor): " << extracted << "\n";
std::cout << "Verificação: data[1] = " << data[1] << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 5: Normalização de vetor
std::cout << "\nExemplo 5: Normalização de vetor\n";
MathVector<double> original = {0.2, 0.7, 0.1}; ///< Vetor a ser normalizado.
try {
print_mathvector(original, "Vetor original");
MathVector<double> normalized = VectorSimilarity<double>::normalize(original);
print_mathvector(normalized, "Vetor normalizado");
std::cout << "Magnitude do vetor original: " << original.magnitude() << "\n";
std::cout << "Magnitude do vetor normalizado: " << normalized.magnitude() << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
return 0;
}
Agora que conhecemos o produto escalar, podemos nos aprofundar na matemática que é o coração do processamento de linguagem natural: a multiplicação de matrizes.
3. Multiplicação de Matrizes
A multiplicação de matrizes é uma operação fundamental na álgebra linear que aparece constantemente nos modelos de transformers. Esta operação permite combinar e transformar representações vetoriais, formando a base de diversas operações nos modelos de processamento de linguagem natural.
A multiplicação de matrizes é uma operação que combina duas matrizes para produzir uma nova matriz. É importante notar que a multiplicação de matrizes não é comutativa, ou seja, a ordem das matrizes importa. A multiplicação de matrizes é definida como o produto escalar entre as linhas da primeira matriz e as colunas da segunda matriz.
Formalmente dizemos: sejam $A$ uma matriz de dimensão $m \times n$ e $B$ uma matriz de dimensão $n \times p$. O produto $A \times B$ resultará em uma matriz $C$ de dimensão $m \times p$, na qual cada elemento $c_{ij}$ é determinado pelo produto escalar da $i$-ésima linha de $A$ com a $j$-ésima coluna de $B$:
\[c_{ij} = \sum_{k=1}^{n} a_{ik} \cdot b_{kj}\]A curiosa leitora pode observar que para que a multiplicação de matrizes seja possível, o número de colunas da primeira matriz deve ser igual ao número de linhas da segunda matriz. Esta restrição não é arbitrária, ela garante que os produtos escalares entre linhas e colunas sejam bem definidos.

Figura 2: Visualização da multiplicação de matrizes.
Cada elemento $c_{ij}$ da matriz resultante é obtido pelo produto escalar da linha $i$ da matriz $A$ com a coluna $j$ da matriz $B$.
Nos modelos transformer, a multiplicação de matrizes ocorre com frequência em várias etapas:
- Atenção: as pontuações de atenção nascem do produto $QK^\top$, que veremos na Seção 4.3, no qual cada elemento é o produto escalar entre a query de um token e a key de outro. Uma única multiplicação de matrizes compara todos os pares de posições da sequência de uma só vez, e é isso que torna a operação tão paralelizável;
- Embedding de Tokens: a matriz de embedding transforma cada token discreto em um vetor contínuo de alta dimensão. Se o token for representado como um vetor one-hot, a seleção da linha correspondente da matriz é, formalmente, uma multiplicação matriz-vetor;
- Projeções Lineares: os vetores de query, key e value não existem prontos no texto. Eles são produzidos multiplicando a representação de cada token pelas matrizes de pesos $W_Q$, $W_K$ e $W_V$, aprendidas durante o treinamento;
- Feed-Forward Networks: cada bloco contém duas multiplicações de matrizes separadas por uma função não-linear, e é nelas que mora a maior parte dos parâmetros do modelo;
- Projeções de Saída: a última camada multiplica a representação final por uma matriz de projeção para produzir uma pontuação para cada palavra do vocabulário, transformando geometria em probabilidade.
A eficiência dos modelos transformers deve-se, em parte, à capacidade de paralelizar estas multiplicações de matrizes em hardware especializado, como as GPUs, de Graphics Processing Units (unidades de processamento gráfico), e as TPUs, de Tensor Processing Units (unidades de processamento de tensores).
3.1 Propriedades Importantes
A multiplicação de matrizes possui algumas propriedades notáveis que a diferenciam da multiplicação de números reais:
- Não comutativa: em geral, $A \times B \neq B \times A$. A ordem das operações importa, e veremos um contraexemplo concreto na Seção 3.3.
- Associativa: $(A \times B) \times C = A \times (B \times C)$. Podemos calcular multiplicações sucessivas em qualquer ordem.
- Distributiva sobre a adição: $A \times (B + C) = A \times B + A \times C$.
- Elemento neutro: $A \times I = I \times A = A$, na qual $I$ é a matriz identidade de dimensão apropriada.
3.2 Interpretação Geométrica
Geometricamente, a multiplicação por uma matriz pode ser vista como uma transformação linear no espaço vetorial. Estas transformações podem incluir: rotações, mudança de escala, reflexões, cisalhamentos e projeções. Dependendo da matriz, a transformação pode alterar a posição, a forma ou a orientação dos vetores no espaço.
Nos transformers, estas transformações são aplicadas para mapear representações vetoriais de um espaço para outro, permitindo que a rede aprenda relações complexas entre os elementos da sequência de entrada.
3.3 Exemplo Numérico
Nada como um exemplo numérico para fazer a esforçada leitora sacudir a poeira. Vamos considerar as duas matrizes, $A$ e $B$:
\[A = \begin{bmatrix} 2 & 3 \\ 4 & 1 \end{bmatrix} \quad \text{e} \quad B = \begin{bmatrix} 1 & 5 \\ 2 & 3 \end{bmatrix}\]O produto $C = A \times B$ será:
\[\begin{align} c_{11} &= a_{11} \cdot b_{11} + a_{12} \cdot b_{21} = 2 \times 1 + 3 \times 2 = 2 + 6 = 8 \\ c_{12} &= a_{11} \cdot b_{12} + a_{12} \cdot b_{22} = 2 \times 5 + 3 \times 3 = 10 + 9 = 19 \\ c_{21} &= a_{21} \cdot b_{11} + a_{22} \cdot b_{21} = 4 \times 1 + 1 \times 2 = 4 + 2 = 6 \\ c_{22} &= a_{21} \cdot b_{12} + a_{22} \cdot b_{22} = 4 \times 5 + 1 \times 3 = 20 + 3 = 23 \end{align}\]Portanto:
\[C = A \times B = \begin{bmatrix} 8 & 19 \\ 6 & 23 \end{bmatrix}\]E, como prometido na Seção 3.1, o produto na ordem trocada:
\[B \times A = \begin{bmatrix} 1 & 5 \\ 2 & 3 \end{bmatrix} \begin{bmatrix} 2 & 3 \\ 4 & 1 \end{bmatrix} = \begin{bmatrix} 1 \times 2 + 5 \times 4 & 1 \times 3 + 5 \times 1 \\ 2 \times 2 + 3 \times 4 & 2 \times 3 + 3 \times 1 \end{bmatrix} = \begin{bmatrix} 22 & 8 \\ 16 & 9 \end{bmatrix} \neq A \times B\]Mesmas matrizes, o mesmo número de operações, resultados completamente diferentes. A não comutatividade não é uma curiosidade algébrica: nos transformers, cada multiplicação aplica uma transformação específica em uma ordem específica, e trocá-las produziria um modelo diferente.
3.4 Multiplicação e a Matriz Transposta
Uma propriedade importante que relaciona a multiplicação de matrizes com a operação de transposição é a seguinte: a transposta do produto de duas matrizes é igual ao produto das suas transpostas, mas na ordem inversa.
Formalmente, se $A$ é uma matriz $m \times n$ e $B$ é uma matriz $n \times p$, então o produto $AB$ é uma matriz $m \times p$. A transposta deste produto, $(AB)^\top$, é uma matriz $p \times m$. A propriedade afirma que:
\[(AB)^\top = B^\top A^\top\]Onde $B^\top$ é a transposta de $B$ (dimensão $p \times n$) e $A^\top$ é a transposta de $A$ (dimensão $n \times m$). A multiplicação $B^\top A^\top$ é definida, pois o número de colunas de $B^\top$ ($n$) é igual ao número de linhas de $A^\top$ ($n$), e o resultado é uma matriz $p \times m$, que tem a mesma dimensão de $(AB)^\top$.
Exemplo:
Usando as matrizes do exemplo anterior: $A = \begin{bmatrix} 2 & 3 \ 4 & 1 \end{bmatrix}$ e $B = \begin{bmatrix} 1 & 5 \ 2 & 3 \end{bmatrix}$.
Calculamos $AB = \begin{bmatrix} 8 & 19 \ 6 & 23 \end{bmatrix}$. A transposta é $(AB)^\top = \begin{bmatrix} 8 & 6 \ 19 & 23 \end{bmatrix}$.
Agora, calculamos as transpostas de A e B: $A^\top = \begin{bmatrix} 2 & 4 \ 3 & 1 \end{bmatrix}$ e $B^\top = \begin{bmatrix} 1 & 2 \ 5 & 3 \end{bmatrix}$.
Multiplicando as transpostas na ordem inversa: \(B^\top A^\top = \begin{bmatrix} 1 & 2 \\ 5 & 3 \end{bmatrix} \begin{bmatrix} 2 & 4 \\ 3 & 1 \end{bmatrix} = \begin{bmatrix} (1 \times 2 + 2 \times 3) & (1 \times 4 + 2 \times 1) \\ (5 \times 2 + 3 \times 3) & (5 \times 4 + 3 \times 1) \end{bmatrix} = \begin{bmatrix} 8 & 6 \\ 19 & 23 \end{bmatrix}\)
Como podemos ver, $(AB)^\top = B^\top A^\top$.
Além dessa identidade fundamental, também podemos realizar multiplicações envolvendo matrizes transpostas diretamente, como $A^\top B$ ou $A B^\top$, desde que as dimensões internas sejam compatíveis. Por exemplo, se $A$ é $m \times n$ e $B$ é $m \times p$, então $A^\top$ tem dimensão $n \times m$. O produto $A^\top B$ é definido porque as dimensões internas são ambas $m$, e o resultado tem dimensão $n \times p$. A compatibilidade dimensional deve sempre ser verificada.
3.5 Multiplicação Matriz-Vetor
Um caso especial e extremamente importante, para nossos objetivos, é a multiplicação de uma matriz por um vetor. A perceptiva leitora há de considerar que um vetor que pode ser visto como uma matriz com apenas uma coluna. Esta operação é recorrente em praticamente todas as camadas de um modelo transformer.
Seja $A$ uma matriz $m \times n$ e $\vec{v}$ um vetor coluna de dimensão $n$. O produto $A\vec{v}$ resulta em um vetor coluna $\vec{w}$ de dimensão $m$:
\[\vec{w} = A\vec{v} = \begin{bmatrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \cdots & a_{mn} \end{bmatrix} \begin{bmatrix} v_1 \\ v_2 \\ \vdots \\ v_n \end{bmatrix} = \begin{bmatrix} \sum_{j=1}^{n} a_{1j}v_j \\ \sum_{j=1}^{n} a_{2j}v_j \\ \vdots \\ \sum_{j=1}^{n} a_{mj}v_j \end{bmatrix}\]Cada componente $w_i$ do vetor resultante é o produto escalar da $i$-ésima linha da matriz $A$ com o vetor $\vec{v}$. Este padrão de operação repete-se continuamente no mecanismo de atenção dos transformers.
Exemplo: Considerando a matriz $A$ definida acima e o vetor $\vec{v} = \begin{bmatrix} 3 \ 2 \end{bmatrix}$, temos:
\[A\vec{v} = \begin{bmatrix} 2 & 3 \\ 4 & 1 \end{bmatrix} \begin{bmatrix} 3 \\ 2 \end{bmatrix} = \begin{bmatrix} (2 \times 3) + (3 \times 2) \\ (4 \times 3) + (1 \times 2) \end{bmatrix} = \begin{bmatrix} 6 + 6 \\ 12 + 2 \end{bmatrix} = \begin{bmatrix} 12 \\ 14 \end{bmatrix}\]3.6 Exemplo de Operações de Multiplicação de Matrizes em C++
#include <iostream> ///< Para entrada e saída padrão (std::cout, std::cerr).
#include <vector> ///< Para contêiner std::vector usado no armazenamento de elementos da matriz.
#include <iomanip> ///< Para std::fixed e std::setprecision, usados na formatação de saída.
#include <stdexcept> ///< Para exceções padrão como std::out_of_range e std::invalid_argument.
/**
* @class Matrix
* @brief Uma classe genérica para representar e manipular matrizes.
*
* Suporta operações como multiplicação de matrizes, multiplicação por vetor e impressão formatada.
* A matriz é armazenada como um vetor de vetores de tipo T.
*
* @tparam T O tipo dos elementos da matriz (deve suportar operações aritméticas).
*/
template<typename T>
class Matrix {
private:
std::vector<std::vector<T>> data; ///< Armazenamento interno para os elementos da matriz.
size_t rows; ///< Número de linhas da matriz.
size_t cols; ///< Número de colunas da matriz.
public:
/**
* @brief Construtor para criar uma matriz de dimensões m x n com valor inicial.
* @param m Número de linhas.
* @param n Número de colunas.
* @param initial_value Valor inicial para todos os elementos (padrão é T{}).
*/
Matrix(size_t m, size_t n, T initial_value = T{})
: rows(m), cols(n), data(m, std::vector<T>(n, initial_value)) {}
/**
* @brief Construtor a partir de um vetor de vetores.
* @param values Os dados de entrada como um vetor de vetores.
* @throws std::invalid_argument Se as linhas tiverem tamanhos inconsistentes.
*/
Matrix(const std::vector<std::vector<T>>& values) {
if (values.empty()) {
rows = 0;
cols = 0;
return;
}
rows = values.size();
cols = values[0].size();
// Verifica se todas as linhas têm o mesmo tamanho
for (const auto& row : values) {
if (row.size() != cols) {
throw std::invalid_argument("Todas as linhas devem ter o mesmo número de colunas");
}
}
data = values; // Copia os dados
}
/**
* @brief Fornece acesso não constante a um elemento da matriz.
* @param i Índice da linha.
* @param j Índice da coluna.
* @return Referência ao elemento na posição (i, j).
* @throws std::out_of_range Se os índices estiverem fora dos limites.
*/
T& at(size_t i, size_t j) {
if (i >= rows || j >= cols) {
throw std::out_of_range("Índices fora dos limites da matriz");
}
return data[i][j];
}
/**
* @brief Fornece acesso constante a um elemento da matriz.
* @param i Índice da linha.
* @param j Índice da coluna.
* @return Referência constante ao elemento na posição (i, j).
* @throws std::out_of_range Se os índices estiverem fora dos limites.
*/
const T& at(size_t i, size_t j) const {
if (i >= rows || j >= cols) {
throw std::out_of_range("Índices fora dos limites da matriz");
}
return data[i][j];
}
/**
* @brief Retorna o número de linhas da matriz.
* @return O número de linhas.
*/
size_t num_rows() const { return rows; }
/**
* @brief Retorna o número de colunas da matriz.
* @return O número de colunas.
*/
size_t num_cols() const { return cols; }
/**
* @brief Multiplica esta matriz por outra.
* @param other A matriz a ser multiplicada.
* @return A matriz resultante.
* @throws std::invalid_argument Se as dimensões forem incompatíveis.
*/
Matrix<T> operator*(const Matrix<T>& other) const {
if (cols != other.rows) {
throw std::invalid_argument("Dimensões incompatíveis para multiplicação de matrizes");
}
Matrix<T> result(rows, other.cols, T{});
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < other.cols; ++j) {
for (size_t k = 0; k < cols; ++k) {
result.data[i][j] += data[i][k] * other.data[k][j];
}
}
}
return result;
}
/**
* @brief Multiplica a matriz por um vetor (representado como matriz coluna).
* @param vec O vetor de entrada.
* @return O vetor resultante da multiplicação.
* @throws std::invalid_argument Se as dimensões forem incompatíveis.
*/
std::vector<T> multiply_vector(const std::vector<T>& vec) const {
if (cols != vec.size()) {
throw std::invalid_argument("Dimensões incompatíveis para multiplicação matriz-vetor");
}
std::vector<T> result(rows, T{});
for (size_t i = 0; i < rows; ++i) {
for (size_t j = 0; j < cols; ++j) {
result[i] += data[i][j] * vec[j];
}
}
return result;
}
/**
* @brief Imprime a matriz formatada com um nome opcional.
* @param name Nome opcional a ser exibido antes da matriz.
*/
void print(const std::string& name = "") const {
if (!name.empty()) {
std::cout << name << " =\n";
}
for (size_t i = 0; i < rows; ++i) {
std::cout << "[";
for (size_t j = 0; j < cols; ++j) {
std::cout << std::fixed << std::setprecision(2) << data[i][j];
if (j < cols - 1) std::cout << ", ";
}
std::cout << "]\n";
}
}
};
/**
* @brief Função principal que demonstra operações de multiplicação de matrizes.
*
* Este programa ilustra a multiplicação de matrizes, a multiplicação de matriz por vetor
* e o tratamento de erros para dimensões incompatíveis usando a classe Matrix.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(2);
std::cout << "Demonstração de Multiplicação de Matrizes\n";
std::cout << "----------------------------------------\n\n";
// Exemplo 1: Multiplicação de duas matrizes
Matrix<double> A({
{2.0, 3.0},
{4.0, 1.0}
}); ///< Matriz A (2x2) para demonstração.
Matrix<double> B({
{1.0, 5.0},
{2.0, 3.0}
}); ///< Matriz B (2x2) para demonstração.
std::cout << "Exemplo 1: Multiplicação de duas matrizes\n";
A.print("Matriz A");
B.print("Matriz B");
try {
Matrix<double> C = A * B;
C.print("A * B");
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << '\n';
}
// Exemplo 2: Multiplicação matriz-vetor
std::vector<double> v = {3.0, 2.0}; ///< Vetor v (2x1) para demonstração.
std::cout << "\nExemplo 2: Multiplicação matriz-vetor\n";
A.print("Matriz A");
std::cout << "Vetor v = [" << v[0] << ", " << v[1] << "]\n";
try {
std::vector<double> result = A.multiply_vector(v);
std::cout << "A * v = [";
for (size_t i = 0; i < result.size(); ++i) {
std::cout << result[i];
if (i < result.size() - 1) std::cout << ", ";
}
std::cout << "]\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << '\n';
}
// Exemplo 3: Demonstração de erro (dimensões incompatíveis)
Matrix<double> D({
{1.0, 2.0, 3.0},
{4.0, 5.0, 6.0}
}); ///< Matriz D (2x3) para demonstração de erro.
std::cout << "\nExemplo 3: Tentativa de multiplicação com dimensões incompatíveis\n";
A.print("Matriz A (2x2)");
D.print("Matriz D (2x3)");
try {
Matrix<double> E = D * A; // Isso deve falhar (3 colunas × 2 linhas)
E.print("D * A");
} catch (const std::exception& e) {
std::cout << "Erro (esperado): " << e.what() << '\n';
}
return 0;
}
Ou, como a preocupada leitora pode preferir, em C++ usando a biblioteca Eigen:
#include <iostream> ///< Para entrada e saída padrão (std::cout, std::cerr).
#include <iomanip> ///< Para std::fixed e std::setprecision, usados na formatação de saída.
#include <Eigen/Dense> ///< Para a biblioteca Eigen, usada em operações de álgebra linear.
#include <string> ///< Para std::string, usada em mensagens de erro.
#include <stdexcept> ///< Para exceções padrão como std::invalid_argument.
/**
* @brief Função principal que demonstra operações de multiplicação de matrizes usando a biblioteca Eigen.
*
* Este programa ilustra três exemplos: a multiplicação de duas matrizes, a multiplicação de uma matriz por um vetor
* e uma tentativa de multiplicação com dimensões incompatíveis, demonstrando o tratamento de erros. As operações são
* realizadas utilizando a biblioteca Eigen, e os resultados são exibidos no console com formatação de duas casas decimais.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(2); ///< Define precisão fixa de duas casas decimais para saída.
std::cout << "Demonstração de Multiplicação de Matrizes\n";
std::cout << "----------------------------------------\n\n";
// Exemplo 1: Multiplicação de duas matrizes
Eigen::Matrix2d A; ///< Matriz A (2x2) para demonstração.
A << 2.0, 3.0,
4.0, 1.0;
Eigen::Matrix2d B; ///< Matriz B (2x2) para demonstração.
B << 1.0, 5.0,
2.0, 3.0;
std::cout << "Exemplo 1: Multiplicação de duas matrizes\n";
std::cout << "Matriz A =\n" << A << "\n\n";
std::cout << "Matriz B =\n" << B << "\n\n";
try {
Eigen::Matrix2d C = A * B; ///< Calcula o produto A * B.
std::cout << "A * B =\n" << C << "\n\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << '\n';
}
// Exemplo 2: Multiplicação matriz-vetor
Eigen::Vector2d v; ///< Vetor v (2x1) para demonstração.
v << 3.0, 2.0;
std::cout << "Exemplo 2: Multiplicação matriz-vetor\n";
std::cout << "Matriz A =\n" << A << "\n\n";
std::cout << "Vetor v =\n" << v << "\n\n";
try {
Eigen::Vector2d result = A * v; ///< Calcula o produto A * v.
std::cout << "A * v =\n" << result << "\n\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << '\n';
}
// Exemplo 3: Demonstração de erro (dimensões incompatíveis)
Eigen::MatrixXd D(2, 3); ///< Matriz D (2x3) para demonstração de erro.
D << 1.0, 2.0, 3.0,
4.0, 5.0, 6.0;
std::cout << "Exemplo 3: Tentativa de multiplicação com dimensões incompatíveis\n";
std::cout << "Matriz A (2x2) =\n" << A << "\n\n";
std::cout << "Matriz D (2x3) =\n" << D << "\n\n";
try {
// Verificação explícita de dimensões (para ser didático)
if (D.cols() != A.rows()) {
throw std::invalid_argument("Dimensões incompatíveis: D é 2x3 e A é 2x2");
}
// Eigen lançará uma asserção estática aqui se compilado com verificações
Eigen::MatrixXd E = D * A; ///< Tentativa de calcular D * A (deve falhar).
std::cout << "D * A =\n" << E << "\n\n";
} catch (const std::exception& e) {
std::cout << "Erro (esperado): " << e.what() << '\n';
}
return 0;
}
A multiplicação de matrizes, porém, é apenas metade da história. Para que um modelo aprenda, é preciso saber como cada número dentro dessas matrizes afeta o resultado final. É disso que trata a próxima seção.
4. Operações de Matrizes Diferenciáveis: Fundamentos e Aplicações
Vou forçar um pouco a amizade. Para entender os transformers, a esforçada leitora precisará entender operações diferenciáveis com matrizes. Estas operações são essenciais para entender os mecanismos de aprendizado profundo, cálculo de gradientes e otimização computacional. Todas são usadas na implementação dos algoritmos que conhecemos como transformers.
As operações de matrizes diferenciáveis são funções que mapeiam matrizes para matrizes, ou para escalares, mantendo propriedades de diferenciabilidade. Em outras palavras, são operações para as quais podemos calcular derivadas ou gradientes. Este conceito é o coração matemático que permite o funcionamento de algoritmos de otimização, particularmente em redes neurais modernas.
Derivada vs. Gradiente
A derivada é um conceito do cálculo que mede a taxa de variação de uma função $f(x)$ de uma única variável. Formalmente, a derivada de $f$ no ponto $x$ é definida como:
\[f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}\]O gradiente, por sua vez, é a generalização da derivada para funções de múltiplas variáveis. Para uma função $f(x_1, x_2, …, x_n)$, o gradiente é um vetor cujas componentes são as derivadas parciais da função em relação a cada variável:
\[\nabla f = \left(\frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, ..., \frac{\partial f}{\partial x_n}\right)\]Diferenças fundamentais: a derivada é um escalar, enquanto o gradiente é um vetor. Além disso, a derivada indica a direção e taxa de crescimento em uma dimensão, enquanto o gradiente aponta na direção de máximo crescimento da função no espaço multidimensional. Finalmente, para funções de uma única variável, o gradiente se reduz à derivada comum.
O gradiente é particularmente importante em otimização, pois seu oposto indica a direção de descida mais íngreme, fundamental para algoritmos como o gradiente descendente usado no treinamento de modelos de aprendizado de máquina.
Ao contrário de operações discretas, as operações diferenciáveis permitem ajustes contínuos e graduais, essenciais para a convergência de algoritmos de aprendizado. Pense nas operações diferenciáveis como uma bússola matemática que guia o modelo através de um oceano de parâmetros em direção ao mínimo de uma função de perda.
Formalmente, consideremos uma função $f: \mathbb{R}^{m \times n} \rightarrow \mathbb{R}$ que mapeia uma matriz $X \in \mathbb{R}^{m \times n}$ para um escalar. Esta função é diferenciável em um ponto $X$ se existe uma matriz $\nabla f(X) \in \mathbb{R}^{m \times n}$, chamada gradiente de $f$ em $X$, tal que:
\[\lim_{H \rightarrow 0} \frac{f(X + H) - f(X) - \langle \nabla f(X), H \rangle_F}{\|H\|_F} = 0\]Nesta expressão, temos:
- $H$ representa uma perturbação infinitesimal na matriz $X$;
- $\langle A, B \rangle_F = \text{tr}(A^\top B)$ é o produto interno de Frobenius entre matrizes;
- $|H|F = \sqrt{\sum{i,j} H_{ij}^2}$ é a norma de Frobenius da matriz $H$.
O produto interno de Frobenius é essencialmente o produto escalar generalizado para matrizes. Se interpretarmos as matrizes $A$ e $B$ como vetores “esticados”, o produto interno de Frobenius é equivalente ao produto escalar desses vetores.
A norma de Frobenius, por sua vez, é a generalização da norma euclidiana para matrizes, e mede a “magnitude” total de uma matriz.
4.1 Operações Fundamentais e Suas Derivadas
Vamos examinar as principais operações de matrizes diferenciáveis e suas derivadas correspondentes. Para cada operação, apresentaremos a definição formal, a interpretação intuitiva e exemplos numéricos quando apropriado.
-
Adição de Matrizes: A adição de matrizes é uma operação elementar onde somamos correspondentemente os elementos de duas matrizes de mesmas dimensões. Sejam $A, B \in \mathbb{R}^{m \times n}$, a adição será definida como:
\[f(A, B) = A + B\]Derivadas:
- $\frac{\partial f}{\partial A} = I$ (matriz identidade);
- $\frac{\partial f}{\partial B} = I$ (matriz identidade).
Isto significa que uma pequena mudança em qualquer elemento de $A$ ou $B$ resulta em uma mudança exatamente igual no elemento correspondente da matriz resultante. Esta propriedade reflete a natureza linear e direta da adição.
Exemplo Numérico: considerando $A = \begin{bmatrix} 1 & 2 \ 3 & 4 \end{bmatrix}$ e $B = \begin{bmatrix} 5 & 6 \ 7 & 8 \end{bmatrix}$, temos:
\[A + B = \begin{bmatrix} 1+5 & 2+6 \\ 3+7 & 4+8 \end{bmatrix} = \begin{bmatrix} 6 & 8 \\ 10 & 12 \end{bmatrix}\]Se perturbarmos $A_{11}$ por uma pequena quantidade $\epsilon$, o resultado será alterado exatamente por $\epsilon$ no elemento $(1,1)$.
-
Multiplicação de Matrizes: a multiplicação matricial é uma operação fundamental que combina informações de duas matrizes através de produtos escalares entre linhas e colunas. Sejam $A \in \mathbb{R}^{m \times n}$ e $B \in \mathbb{R}^{n \times p}$, a multiplicação será definida como:
\[f(A, B) = AB\]Derivadas: Para uma matriz resultante $C = AB$, na qual $A \in \mathbb{R}^{m \times n}$ e $B \in \mathbb{R}^{n \times p}$:
- $\frac{\partial C_{ij}}{\partial A_{kl}} = B_{lj}$ se $i = k$, e $0$ caso contrário;
- $\frac{\partial C_{ij}}{\partial B_{kl}} = A_{ik}$ se $j = l$, e $0$ caso contrário.
Em notação tensorial mais compacta:
- $\frac{\partial (AB){ij}}{\partial A{kl}} = \delta_{ik} B_{lj}$;
- $\frac{\partial (AB){ij}}{\partial B{kl}} = A_{ik} \delta_{jl}$.
Neste caso, $\delta_{ij}$ é o delta de Kronecker, igual a $1$ se $i = j$ e $0$ caso contrário.
Para que a esforçada leitora possa entender isso considere que o elemento $C_{ij}$ depende de toda a $i$-ésima linha de $A$ e da $j$-ésima coluna de $B$. Uma mudança em $A_{il}$ afetará $C_{ij}$ proporcionalmente a $B_{lj}$.
Exemplo Numérico: considerando $A = \begin{bmatrix} 2 & 3 \ 4 & 1 \end{bmatrix}$ e $B = \begin{bmatrix} 1 & 5 \ 2 & 3 \end{bmatrix}$, temos:
\[AB = \begin{bmatrix} 2 \times 1 + 3 \times 2 & 2 \times 5 + 3 \times 3 \\ 4 \times 1 + 1 \times 2 & 4 \times 5 + 1 \times 3 \end{bmatrix} = \begin{bmatrix} 8 & 19 \\ 6 & 23 \end{bmatrix}\]Se aumentarmos $A_{12}$ (o elemento $3$) por $0.1$, o novo valor será:
\[A = \begin{bmatrix} 2 & 3.1 \\ 4 & 1 \end{bmatrix}\]E o produto atualizado:
\[AB = \begin{bmatrix} 2 \times 1 + 3.1 \times 2 & 2 \times 5 + 3.1 \times 3 \\ 4 \times 1 + 1 \times 2 & 4 \times 5 + 1 \times 3 \end{bmatrix} = \begin{bmatrix} 8.2 & 19.3 \\ 6 & 23 \end{bmatrix}\]Observe que apenas os elementos da primeira linha foram alterados, e a mudança em $C_{11}$ foi de $0.1 \times B_{21} = 0.1 \times 2 = 0.2$, enquanto a mudança em $C_{12}$ foi de $0.1 \times B_{22} = 0.1 \times 3 = 0.3$.
-
Traço de Matriz: o traço de uma matriz quadrada é a soma dos elementos da diagonal principal. É uma operação que reduz uma matriz a um escalar. Seja $A \in \mathbb{R}^{n \times n}$, o traço será definido como:
\[f(A) = \text{tr}(A)\]Derivada:
- $\frac{\partial \text{tr}(A)}{\partial A} = I$ (matriz identidade).
Isto significa que a derivada do traço em relação a cada elemento da diagonal é $1$, e em relação aos elementos fora da diagonal é $0$.
Exemplo Numérico: para $A = \begin{bmatrix} 3 & 2 \ 1 & 5 \end{bmatrix}$, temos $\text{tr}(A) = 3 + 5 = 8$.
Aumentando $A_{11}$ por 0.1, o novo traço será $3.1 + 5 = 8.1$, confirmando que $\frac{\partial \text{tr}(A)}{\partial A_{11}} = 1$.
-
Determinante de Matriz: o determinante é uma função escalar de uma matriz quadrada que tem interpretações geométricas importantes e está relacionado a propriedades como inversibilidade. Seja $A \in \mathbb{R}^{n \times n}$, o determinante será definido como:
\[f(A) = \det(A)\]Derivada:
- $\frac{\partial \det(A)}{\partial A} = \det(A) \cdot (A^{-1})^\top$.
Esta fórmula é válida para matrizes invertíveis. Intuitivamente, a sensitividade do determinante a mudanças em $A$ depende do próprio determinante e da inversa de $A$.
Para uma matriz $2 \times 2$, $A = \begin{bmatrix} a & b \ c & d \end{bmatrix}$, temos fórmulas explícitas:
- $\frac{\partial \det(A)}{\partial a} = d$;
- $\frac{\partial \det(A)}{\partial b} = -c$;
- $\frac{\partial \det(A)}{\partial c} = -b$;
- $\frac{\partial \det(A)}{\partial d} = a$.
Exemplo Numérico: Para $A = \begin{bmatrix} 3 & 2 \ 1 & 5 \end{bmatrix}$, temos $\det(A) = 3 \times 5 - 2 \times 1 = 13$.
Aumentando $A_{11}$ por 0.1, o novo determinante será: $\det\begin{bmatrix} 3.1 & 2 \ 1 & 5 \end{bmatrix} = 3.1 \times 5 - 2 \times 1 = 15.5 - 2 = 13.5$
A mudança foi de 0.5, que corresponde a $\frac{\partial \det(A)}{\partial A_{11}} = A_{22} = 5$, como esperado.
-
Inversa de Matriz: a inversa de uma matriz é uma operação fundamental que transforma $A$ em $A^{-1}$ tal que $AA^{-1} = A^{-1}A = I$.
\[f(A) = A^{-1}\]Derivada: A derivada da inversa em relação aos elementos da matriz original envolve a própria inversa:
\[\frac{\partial (A^{-1})_{ij}}{\partial A_{kl}} = -(A^{-1})_{ik}(A^{-1})_{lj}\]Em notação matricial mais compacta:
\[\frac{\partial A^{-1}}{\partial A_{kl}} = -A^{-1} E_{kl} A^{-1}\]Na qual, $E_{kl}$ é uma matriz com 1 na posição $(k,l)$ e 0 nas demais.
Esta fórmula demonstra como pequenas mudanças em $A$ propagam não-linearmente para sua inversa, através de um “sanduíche” de multiplicações matriciais.
Exemplo Numérico: para $A = \begin{bmatrix} 2 & 1 \ 1 & 1 \end{bmatrix}$, a inversa é $A^{-1} = \begin{bmatrix} 1 & -1 \ -1 & 2 \end{bmatrix}$.
Se perturbarmos $A_{11}$ por $0.1$:
\[A = \begin{bmatrix} 2.1 & 1 \\ 1 & 1 \end{bmatrix}\]A nova inversa seria:
\[A^{-1} = \begin{bmatrix} 0.9091 & -0.9091 \\ -0.9091 & 1.9091 \end{bmatrix} \text{(arredondado para 4 casas decimais)}\]A mudança em $(A^{-1})_{11}$ é aproximadamente $-0.0909$, que está alinhada com a fórmula da derivada.
4.2 Propriedades Úteis e Identidades
Além das derivadas básicas acima, existem algumas identidades do cálculo com matrizes que serão úteis para o entendimento de operações diferenciáveis especialmente nos algoritmos que envolvem aprendizado profundo. Algumas delas incluem:
-
Regra da Cadeia para Funções Matriciais:
Se $f(X) = g(h(X))$, o gradiente de $f$ é obtido compondo as derivadas de $g$ e $h$, exatamente como no cálculo de uma variável; a forma matricial exata depende das dimensões de cada função envolvida. É esta composição, aplicada camada a camada, que a retropropagação explora nas redes neurais profundas.
-
Derivada do Traço de um Produto:
\[\frac{\partial \text{tr}(AB)}{\partial A} = B^\top\] \[\frac{\partial \text{tr}(AB)}{\partial B} = A^\top\] -
Derivada do Traço de uma Transformação Quadrática:
\[\frac{\partial \text{tr}(AXB)}{\partial X} = A^\top B^\top\] -
Derivada do Traço de uma Forma Quadrática:
\[\frac{\partial \text{tr}(X^\top A X)}{\partial X} = (A + A^\top)X\]
Estas identidades são particularmente úteis em otimização, onde muitas funções de perda podem ser expressas em termos de traços.
4.3 Aplicações em Aprendizado Profundo e Transformers
As operações de matrizes diferenciáveis são o fundamento matemático do aprendizado profundo. Elas permitem:
- Retropropagação em Redes Neurais: a retropropagação (backpropagation) é um algoritmo que utiliza a regra da cadeia para calcular gradientes em redes neurais. As operações matriciais diferenciáveis permitem o fluxo suave de informação do gradiente através das camadas da rede. Considerando um modelo simples:
no qual $W$ é uma matriz de pesos, $b$ é um vetor de viés, $x$ é o vetor de entrada e $\sigma$ é uma função de ativação não-linear.
Para uma função de perda $L$, calculamos:
\[\frac{\partial L}{\partial W} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial Wx} \cdot \frac{\partial (Wx)}{\partial W}\]Cada uma dessas derivadas envolve operações matriciais diferenciáveis que permitem o ajuste gradual dos pesos em direção ao mínimo da função de perda.
- Otimização de Matrizes de Embedding: em processamento de linguagem natural, as palavras são frequentemente representadas como vetores de embedding. Estes embeddings são linhas, vetores, de uma matriz $E$ que é otimizada através de operações diferenciáveis. Por exemplo, no modelo word2vec, a probabilidade de uma palavra de saída $w_O$ dada uma palavra de entrada $w_I$ será calculada como:
neste caso, $v_{w_I}$ e $v_{w_O}$ são vetores de embedding para palavras de entrada e saída, respectivamente.
A otimização destes embeddings requer o cálculo de gradientes de operações como produtos escalares e multiplicações de matrizes.
- Mecanismos de Atenção nos Transformers: nos modelos Transformer, o mecanismo de atenção utiliza extensivamente operações matriciais diferenciáveis. A atenção é calculada como:
Na qual, $Q$, $K$, e $V$ são matrizes de consulta, chave e valor. O termo $d_k$ é a dimensão dos vetores de chave, e a divisão por $\sqrt{d_k}$ é um fator de escala que evita que o produto $QK^\top$ cresça demais quando $d_k$ é grande, mantendo os gradientes em uma faixa numérica estável. A função $\text{softmax}$, por sua vez, transforma cada linha da matriz de pontuações em uma distribuição de probabilidade, na qual todos os valores ficam entre $0$ e $1$ e somam $1$. Discutiremos a atenção em detalhe nos próximos artigos da série; por ora, basta notar que toda a expressão é construída a partir das operações que estudamos aqui: multiplicação de matrizes, transposição e produto escalar.
O cálculo dos gradientes para atualizar estas matrizes depende fundamentalmente das derivadas das operações de multiplicação matricial, transposição e outras operações diferenciáveis discutidas anteriormente.
4.4 Do erro ao gradiente dos pesos
O último exemplo do programa da Seção 4.5 executará uma etapa mínima de retropropagação. Para que nenhuma operação apareça primeiro em C++, deduziremos as três linhas centrais antes de encontrá-las no código: o erro vetorial, a perda quadrática e o produto externo que produz o gradiente da matriz de pesos.
Considere uma camada linear com matriz de pesos $W\in\mathbb{R}^{2\times2}$, entrada $x\in\mathbb{R}^2$ e saída desejada $y_{\text{true}}\in\mathbb{R}^2$:
\[W=\begin{bmatrix}1&0{,}5\\0{,}3&1{,}2\end{bmatrix},\qquad x=\begin{bmatrix}0{,}4\\0{,}6\end{bmatrix},\qquad y_{\text{true}}=\begin{bmatrix}1\\0\end{bmatrix}.\]Na passagem direta, ou forward pass, a predição é o produto matriz-vetor já definido na Seção 3.5:
\[y_{\text{pred}}=Wx =\begin{bmatrix}1\cdot0{,}4+0{,}5\cdot0{,}6\\0{,}3\cdot0{,}4+1{,}2\cdot0{,}6\end{bmatrix} =\begin{bmatrix}0{,}7\\0{,}84\end{bmatrix}.\]Definimos o vetor de erro como $e=y_{\text{true}}-y_{\text{pred}}$. A perda quadrática $L$ é a norma euclidiana de $e$ elevada ao quadrado, ou, de modo equivalente, o produto escalar de $e$ consigo mesmo:
\[e=\begin{bmatrix}0{,}3\\-0{,}84\end{bmatrix},\qquad L=\lVert e\rVert_2^2=e^\top e =0{,}3^2+(-0{,}84)^2 =0{,}7956.\]A escrita $e^\top e$ também explica o método squaredNorm() da biblioteca Eigen: ele calcula a soma dos quadrados sem extrair uma raiz que seria imediatamente elevada ao quadrado outra vez. Para obter o gradiente em relação à predição, derivamos cada parcela $(y_{\text{true},i}-y_{\text{pred},i})^2$:
Portanto, chamando esse gradiente de $g$,
\[g=\frac{\partial L}{\partial y_{\text{pred}}} =-2e =\begin{bmatrix}-0{,}6\\1{,}68\end{bmatrix}.\]Resta transformar um gradiente com duas componentes em um gradiente com o mesmo formato $2\times2$ de $W$. Como $y_{\text{pred},i}=\sum_j W_{ij}x_j$, temos $\partial y_{\text{pred},i}/\partial W_{ij}=x_j$. A regra da cadeia fornece
\[\left[\frac{\partial L}{\partial W}\right]_{ij}=g_i x_j.\]Esta operação chama-se produto externo: se $g\in\mathbb{R}^m$ é um vetor coluna e $x^\top\in\mathbb{R}^{1\times n}$ é um vetor linha, então $g x^\top\in\mathbb{R}^{m\times n}$ é uma matriz cujo elemento $(i,j)$ vale $g_i x_j$. Para os nossos números,
\[\frac{\partial L}{\partial W} =g x^\top =\begin{bmatrix}-0{,}6\\1{,}68\end{bmatrix} \begin{bmatrix}0{,}4&0{,}6\end{bmatrix} =\begin{bmatrix}-0{,}24&-0{,}36\\0{,}672&1{,}008\end{bmatrix}.\]Agora as três expressões do C++ têm antepassados matemáticos visíveis: error.squaredNorm() calcula $e^\top e$; -2.0 * error calcula $g=-2e$; e dL_dy_pred * x.transpose() calcula o produto externo $g x^\top$. A transposição não é enfeite sintático: ela muda $x$ de $2\times1$ para $1\times2$, tornando o produto $(2\times1)(1\times2)$ uma matriz $2\times2$, exatamente o formato de $W$.
4.5 Exemplo de Operações Matriciais Diferenciáveis em C++
#include <iostream> ///< Para entrada e saída padrão (std::cout, std::cerr).
#include <vector> ///< Para contêiner std::vector usado em matrizes manuais.
#include <iomanip> ///< Para std::fixed e std::setprecision, usados na formatação de saída.
#include <stdexcept> ///< Para exceções padrão como std::invalid_argument.
#include <Eigen/Dense> ///< Para a biblioteca Eigen, usada em operações matriciais.
#include <cmath> ///< Para std::abs, usado em comparações de ponto flutuante.
/**
* @class DifferentiableMatrixOperations
* @brief Uma classe para demonstrar operações matriciais diferenciáveis e suas derivadas.
*
* Implementa operações como adição, multiplicação, traço, determinante e inversa de matrizes,
* com métodos para calcular derivadas aproximadas numericamente. Usa a biblioteca Eigen para
* operações eficientes e fornece exemplos numéricos para ilustrar a diferenciabilidade.
*/
class DifferentiableMatrixOperations {
private:
static constexpr double epsilon = 1e-6; ///< Pequena perturbação para cálculo numérico de derivadas.
/**
* @brief Imprime uma matriz Eigen com um nome opcional.
* @param matrix A matriz Eigen a ser impressa.
* @param name Nome opcional a ser exibido antes da matriz.
*/
template<typename Derived>
static void printMatrix(const Eigen::MatrixBase<Derived>& matrix, const std::string& name = "") {
if (!name.empty()) {
std::cout << name << " =\n";
}
std::cout << matrix << "\n";
}
public:
/**
* @brief Realiza a adição de duas matrizes e calcula derivadas parciais aproximadas.
* @param A Primeira matriz (Eigen::MatrixXd).
* @param B Segunda matriz (Eigen::MatrixXd).
* @return A matriz resultante da adição.
* @throws std::invalid_argument Se as dimensões forem incompatíveis.
*/
static Eigen::MatrixXd matrixAddition(const Eigen::MatrixXd& A, const Eigen::MatrixXd& B) {
if (A.rows() != B.rows() || A.cols() != B.cols()) {
throw std::invalid_argument("Dimensões incompatíveis para adição de matrizes");
}
return A + B;
}
/**
* @brief Demonstra a derivada da adição de matrizes (∂(A+B)/∂A = I, ∂(A+B)/∂B = I).
* @param A Matriz de entrada para perturbação.
* @param B Matriz fixa.
* @param i Índice da linha do elemento a perturbar.
* @param j Índice da coluna do elemento a perturbar.
* @return Matriz de derivadas aproximadas para ∂(A+B)/∂A(i,j).
*/
static Eigen::MatrixXd derivativeMatrixAddition(const Eigen::MatrixXd& A, const Eigen::MatrixXd& B, int i, int j) {
Eigen::MatrixXd A_perturbed = A;
A_perturbed(i, j) += epsilon;
Eigen::MatrixXd result = matrixAddition(A_perturbed, B);
Eigen::MatrixXd base = matrixAddition(A, B);
return (result - base) / epsilon; // Derivada numérica: [f(A+h) - f(A)] / h
}
/**
* @brief Realiza a multiplicação de duas matrizes.
* @param A Primeira matriz (Eigen::MatrixXd).
* @param B Segunda matriz (Eigen::MatrixXd).
* @return A matriz resultante da multiplicação.
* @throws std::invalid_argument Se as dimensões forem incompatíveis.
*/
static Eigen::MatrixXd matrixMultiplication(const Eigen::MatrixXd& A, const Eigen::MatrixXd& B) {
if (A.cols() != B.rows()) {
throw std::invalid_argument("Dimensões incompatíveis para multiplicação de matrizes");
}
return A * B;
}
/**
* @brief Demonstra a derivada da multiplicação de matrizes (∂(AB)/∂A, ∂(AB)/∂B).
* @param A Matriz de entrada para perturbação.
* @param B Matriz fixa.
* @param i Índice da linha do elemento a perturbar.
* @param j Índice da coluna do elemento a perturbar.
* @param with_respect_to Indica se a derivada é em relação a A ("A") ou B ("B").
* @return Matriz de derivadas aproximadas.
*/
static Eigen::MatrixXd derivativeMatrixMultiplication(const Eigen::MatrixXd& A, const Eigen::MatrixXd& B,
int i, int j, const std::string& with_respect_to) {
if (with_respect_to == "A") {
Eigen::MatrixXd A_perturbed = A;
A_perturbed(i, j) += epsilon;
Eigen::MatrixXd result = matrixMultiplication(A_perturbed, B);
Eigen::MatrixXd base = matrixMultiplication(A, B);
return (result - base) / epsilon;
} else if (with_respect_to == "B") {
Eigen::MatrixXd B_perturbed = B;
B_perturbed(i, j) += epsilon;
Eigen::MatrixXd result = matrixMultiplication(A, B_perturbed);
Eigen::MatrixXd base = matrixMultiplication(A, B);
return (result - base) / epsilon;
}
throw std::invalid_argument("Parâmetro 'with_respect_to' deve ser 'A' ou 'B'");
}
/**
* @brief Calcula o traço de uma matriz quadrada.
* @param A Matriz quadrada (Eigen::MatrixXd).
* @return O valor escalar do traço.
* @throws std::invalid_argument Se a matriz não for quadrada.
*/
static double matrixTrace(const Eigen::MatrixXd& A) {
if (A.rows() != A.cols()) {
throw std::invalid_argument("Matriz deve ser quadrada para cálculo do traço");
}
return A.trace();
}
/**
* @brief Demonstra a derivada do traço (∂tr(A)/∂A = I).
* @param A Matriz de entrada para perturbação.
* @param i Índice da linha do elemento a perturbar.
* @param j Índice da coluna do elemento a perturbar.
* @return Derivada aproximada do traço em relação a A(i,j).
*/
static double derivativeMatrixTrace(const Eigen::MatrixXd& A, int i, int j) {
Eigen::MatrixXd A_perturbed = A;
A_perturbed(i, j) += epsilon;
double result = matrixTrace(A_perturbed);
double base = matrixTrace(A);
return (result - base) / epsilon;
}
/**
* @brief Calcula o determinante de uma matriz quadrada.
* @param A Matriz quadrada (Eigen::MatrixXd).
* @return O valor escalar do determinante.
* @throws std::invalid_argument Se a matriz não for quadrada.
*/
static double matrixDeterminant(const Eigen::MatrixXd& A) {
if (A.rows() != A.cols()) {
throw std::invalid_argument("Matriz deve ser quadrada para cálculo do determinante");
}
return A.determinant();
}
/**
* @brief Demonstra a derivada do determinante (∂det(A)/∂A = det(A) * (A^-1)^T).
* @param A Matriz de entrada para perturbação.
* @param i Índice da linha do elemento a perturbar.
* @param j Índice da coluna do elemento a perturbar.
* @return Derivada aproximada do determinante em relação a A(i,j).
*/
static double derivativeMatrixDeterminant(const Eigen::MatrixXd& A, int i, int j) {
Eigen::MatrixXd A_perturbed = A;
A_perturbed(i, j) += epsilon;
double result = matrixDeterminant(A_perturbed);
double base = matrixDeterminant(A);
return (result - base) / epsilon;
}
/**
* @brief Calcula a inversa de uma matriz quadrada.
* @param A Matriz quadrada (Eigen::MatrixXd).
* @return A matriz inversa.
* @throws std::invalid_argument Se a matriz não for quadrada ou não for invertível.
*/
static Eigen::MatrixXd matrixInverse(const Eigen::MatrixXd& A) {
if (A.rows() != A.cols()) {
throw std::invalid_argument("Matriz deve ser quadrada para cálculo da inversa");
}
if (std::abs(A.determinant()) < 1e-9) {
throw std::invalid_argument("Matriz não é invertível (determinante próximo de zero)");
}
return A.inverse();
}
/**
* @brief Demonstra a derivada da inversa (∂(A^-1)/∂A).
* @param A Matriz de entrada para perturbação.
* @param i Índice da linha do elemento a perturbar.
* @param j Índice da coluna do elemento a perturbar.
* @return Matriz de derivadas aproximadas para ∂(A^-1)/∂A(i,j).
*/
static Eigen::MatrixXd derivativeMatrixInverse(const Eigen::MatrixXd& A, int i, int j) {
Eigen::MatrixXd A_perturbed = A;
A_perturbed(i, j) += epsilon;
Eigen::MatrixXd result = matrixInverse(A_perturbed);
Eigen::MatrixXd base = matrixInverse(A);
return (result - base) / epsilon;
}
};
/**
* @brief Função principal que demonstra operações matriciais diferenciáveis.
*
* Este programa implementa exemplos numéricos para adição, multiplicação, traço, determinante e inversa
* de matrizes, calculando derivadas aproximadas numericamente. Inclui os exemplos do documento e um caso
* simples de retropropagação para ilustrar a aplicação em aprendizado profundo.
*
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(2);
std::cout << "Demonstração de Operações Matriciais Diferenciáveis\n";
std::cout << "--------------------------------------------------\n\n";
// Exemplo 1: Adição de matrizes
std::cout << "Exemplo 1: Adição de Matrizes e Derivada\n";
Eigen::Matrix2d A1;
A1 << 1.0, 2.0,
3.0, 4.0;
Eigen::Matrix2d B1;
B1 << 5.0, 6.0,
7.0, 8.0;
try {
DifferentiableMatrixOperations::printMatrix(A1, "Matriz A");
DifferentiableMatrixOperations::printMatrix(B1, "Matriz B");
Eigen::MatrixXd sum = DifferentiableMatrixOperations::matrixAddition(A1, B1);
DifferentiableMatrixOperations::printMatrix(sum, "A + B");
// Derivada em relação a A(0,0)
Eigen::MatrixXd deriv_add = DifferentiableMatrixOperations::derivativeMatrixAddition(A1, B1, 0, 0);
DifferentiableMatrixOperations::printMatrix(deriv_add, "∂(A+B)/∂A(0,0)");
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 2: Multiplicação de matrizes
std::cout << "\nExemplo 2: Multiplicação de Matrizes e Derivada\n";
Eigen::Matrix2d A2;
A2 << 2.0, 3.0,
4.0, 1.0;
Eigen::Matrix2d B2;
B2 << 1.0, 5.0,
2.0, 3.0;
try {
DifferentiableMatrixOperations::printMatrix(A2, "Matriz A");
DifferentiableMatrixOperations::printMatrix(B2, "Matriz B");
Eigen::MatrixXd prod = DifferentiableMatrixOperations::matrixMultiplication(A2, B2);
DifferentiableMatrixOperations::printMatrix(prod, "A * B");
// Derivada em relação a A(1,2)
Eigen::MatrixXd deriv_mult_A = DifferentiableMatrixOperations::derivativeMatrixMultiplication(A2, B2, 0, 1, "A");
DifferentiableMatrixOperations::printMatrix(deriv_mult_A, "∂(A*B)/∂A(0,1)");
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 3: Traço de matriz
std::cout << "\nExemplo 3: Traço de Matriz e Derivada\n";
Eigen::Matrix2d A3;
A3 << 3.0, 2.0,
1.0, 5.0;
try {
DifferentiableMatrixOperations::printMatrix(A3, "Matriz A");
double trace = DifferentiableMatrixOperations::matrixTrace(A3);
std::cout << "tr(A) = " << trace << "\n";
// Derivada em relação a A(0,0)
double deriv_trace = DifferentiableMatrixOperations::derivativeMatrixTrace(A3, 0, 0);
std::cout << "∂tr(A)/∂A(0,0) = " << deriv_trace << " (esperado: 1.0)\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 4: Determinante de matriz
std::cout << "\nExemplo 4: Determinante de Matriz e Derivada\n";
Eigen::Matrix2d A4;
A4 << 3.0, 2.0,
1.0, 5.0;
try {
DifferentiableMatrixOperations::printMatrix(A4, "Matriz A");
double det = DifferentiableMatrixOperations::matrixDeterminant(A4);
std::cout << "det(A) = " << det << "\n";
// Derivada em relação a A(0,0)
double deriv_det = DifferentiableMatrixOperations::derivativeMatrixDeterminant(A4, 0, 0);
std::cout << "∂det(A)/∂A(0,0) = " << deriv_det << " (esperado: A(1,1) = 5.0)\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 5: Inversa de matriz
std::cout << "\nExemplo 5: Inversa de Matriz e Derivada\n";
Eigen::Matrix2d A5;
A5 << 2.0, 1.0,
1.0, 1.0;
try {
DifferentiableMatrixOperations::printMatrix(A5, "Matriz A");
Eigen::MatrixXd inv = DifferentiableMatrixOperations::matrixInverse(A5);
DifferentiableMatrixOperations::printMatrix(inv, "A^-1");
// Derivada em relação a A(0,0)
Eigen::MatrixXd deriv_inv = DifferentiableMatrixOperations::derivativeMatrixInverse(A5, 0, 0);
DifferentiableMatrixOperations::printMatrix(deriv_inv, "∂(A^-1)/∂A(0,0)");
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
// Exemplo 6: Retropropagação simples
std::cout << "\nExemplo 6: Retropropagação Simples\n";
Eigen::MatrixXd W(2, 2); // Matriz de pesos
W << 1.0, 0.5,
0.3, 1.2;
Eigen::VectorXd x(2); // Vetor de entrada
x << 0.4, 0.6;
Eigen::VectorXd y_true(2); // Saída desejada
y_true << 1.0, 0.0;
try {
DifferentiableMatrixOperations::printMatrix(W, "Matriz de pesos W");
DifferentiableMatrixOperations::printMatrix(x, "Vetor de entrada x");
DifferentiableMatrixOperations::printMatrix(y_true, "Saída desejada y_true");
// Forward pass: y = W * x
Eigen::VectorXd y_pred = W * x;
DifferentiableMatrixOperations::printMatrix(y_pred, "Saída predita y_pred");
// Função de perda: L = ||y_true - y_pred||^2
Eigen::VectorXd error = y_true - y_pred;
double loss = error.squaredNorm();
std::cout << "Perda L = " << loss << "\n";
// Gradiente da perda em relação a y_pred: ∂L/∂y_pred = -2 * (y_true - y_pred)
Eigen::VectorXd dL_dy_pred = -2.0 * error;
DifferentiableMatrixOperations::printMatrix(dL_dy_pred, "∂L/∂y_pred");
// Gradiente da perda em relação a W: ∂L/∂W = (∂L/∂y_pred) * x^T
Eigen::MatrixXd dL_dW = dL_dy_pred * x.transpose();
DifferentiableMatrixOperations::printMatrix(dL_dW, "∂L/∂W");
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
return 0;
}
5. Transformações Afins: Um Pilar Matemático para Word Embeddings, Transformers e LLMs
Transformações afins são operações matemáticas essenciais em aprendizado de máquina, especialmente em processamento de linguagem natural — notadamente em modelos como transformers, word embeddings e grandes modelos de linguagem, os LLMs, de Large Language Models. Elas permitem a manipulação de vetores e matrizes de maneira a preservar propriedades geométricas como colinearidade e razões de distâncias.
Uma transformação afim é uma função que combina uma transformação linear com uma translação. Uma translação é uma operação geométrica que desloca cada ponto de um espaço por uma mesma distância em uma direção específica, preservando distâncias e ângulos entre pontos. Essencialmente, uma translação é um deslocamento rígido de um objeto sem rotação ou distorção. Formalmente, dada uma matriz $A$, que representa a transformação linear, e um vetor $b$, que representa a translação, a transformação afim $T$ de um vetor $x$ será definida como:
\[T(x) = A x + b\]De forma que:
- $x \in \mathbb{R}^n$ é o vetor de entrada;
- $A \in \mathbb{R}^{m \times n}$ é a matriz de transformação linear;
- $b \in \mathbb{R}^m$ é o vetor de translação;
- $T(x) \in \mathbb{R}^m$ é o vetor resultante.
Essa operação pode incluir rotações, escalonamentos, cisalhamentos e deslocamentos, dependendo das propriedades de $A$ e $b$. Em Processamento de Linguagem Natural, essas transformações são aplicadas a vetores de alta dimensão que representam palavras, tokens ou sequências.
| Contexto | Aplicações de Transformações Afins |
|---|---|
| Word Embeddings | Captura de Relações Semânticas: Transformações lineares mapeiam relações entre palavras (ex: $v_{\text{rainha}} \approx A v_{\text{rei}}$) Normalização e Centralização: Translação $b$ centraliza embeddings; matriz $A$ escala vetores Redução de Dimensionalidade: Técnicas como a PCA, de Principal Component Analysis (análise de componentes principais), aplicam transformações lineares para projetar embeddings em espaços menores |
| Transformers | Mecanismo de Atenção: Matrizes de consulta, chave e valor são geradas por transformações lineares ($Q = W_Q x$, etc.) Camadas Feed-Forward: Cada bloco inclui transformação afim com não-linearidade: $\text{FFN}(x) = W_2 \, \sigma(W_1 x + b_1) + b_2$ Normalização de Camada: Layer normalization ajusta ativações: $y = \gamma \cdot \frac{x - \mu}{\sigma} + \beta$ |
| LLMs | Embeddings de Entrada e Saída: Tokens mapeados para vetores via matriz de embedding; saídas projetadas de volta ao vocabulário Atenção Multi-Cabeça: Cada cabeça aplica transformações lineares independentes, resultados combinados Adaptação a Tarefas: Durante fine-tuning, camadas superiores aplicam transformações afins para tarefas específicas |
Tabela 1: A relevância das transformações afins em Processamento de Linguagem Natural destacando *word embedding*, *transformers* e LLMs.
5.1 Propriedades Importantes
As transformações afins possuem características que as tornam ideais para word embeddings e transformers:
- Preservação de Colinearidade: pontos alinhados permanecem alinhados após a transformação. A parte linear $A$ leva retas em retas, e a translação $b$ apenas as desloca em bloco;
- Preservação de Razões de Distâncias: a proporção entre distâncias ao longo de uma mesma reta é mantida; o ponto médio de um segmento continua sendo o ponto médio do segmento transformado;
- Composição: a combinação de duas transformações afins resulta em outra transformação afim. A conta é direta: $T_2(T_1(x)) = A_2(A_1 x + b_1) + b_2 = (A_2 A_1)x + (A_2 b_1 + b_2)$, que tem exatamente a forma $A’x + b’$. É por isso que empilhar camadas lineares sem não-linearidades entre elas não aumenta o poder expressivo de uma rede;
- Inversibilidade: se $A$ é invertível, $T(x)$ pode ser revertida: $x = A^{-1} (T(x) - b)$.
5.2 Exemplo Numérico
Considere um vetor $x = \begin{bmatrix} 1 \ 2 \end{bmatrix}$, uma matriz $A = \begin{bmatrix} 2 & 1 \ 0 & 3 \end{bmatrix}$ e um vetor $b = \begin{bmatrix} 3 \ 1 \end{bmatrix}$. A transformação afim é:
\[T(x) = A x + b = \begin{bmatrix} 2 & 1 \\ 0 & 3 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \end{bmatrix} + \begin{bmatrix} 3 \\ 1 \end{bmatrix} = \begin{bmatrix} 2 \cdot 1 + 1 \cdot 2 \\ 0 \cdot 1 + 3 \cdot 2 \end{bmatrix} + \begin{bmatrix} 3 \\ 1 \end{bmatrix} = \begin{bmatrix} 4 \\ 6 \end{bmatrix} + \begin{bmatrix} 3 \\ 1 \end{bmatrix} = \begin{bmatrix} 7 \\ 7 \end{bmatrix}\]Esse exemplo ilustra como $A$ aplica uma transformação linear (escalamento e cisalhamento) e $b$ desloca o resultado.
A teoria pode parecer abstrata, mas veja na prática, atenta leitora! Use o painel abaixo para aplicar transformações afins (rotação, translação e escala) e perceba como os pontos se movem no espaço.
5.3 Implementação em C++
Abaixo, apresentamos uma implementação em C++ usando a biblioteca Eigen para aplicar transformações afins, seguindo o estilo do documento fornecido.
#include <iostream>
#include <Eigen/Dense>
#include <iomanip> ///< Adicionando esta biblioteca para std::setprecision
#include <stdexcept>
/**
* @brief Aplica uma transformação afim a um vetor.
* @param A Matriz de transformação linear.
* @param b Vetor de translação.
* @param x Vetor de entrada.
* @return O vetor transformado T(x) = Ax + b.
* @throws std::invalid_argument Se as dimensões forem incompatíveis.
*/
Eigen::VectorXd affineTransform(const Eigen::MatrixXd& A, const Eigen::VectorXd& b, const Eigen::VectorXd& x) {
if (A.cols() != x.size() || A.rows() != b.size()) {
throw std::invalid_argument("Dimensões incompatíveis para transformação afim");
}
return A * x + b;
}
/**
* @brief Função principal que demonstra a aplicação de uma transformação afim.
* @return 0 em caso de execução bem-sucedida.
*/
int main() {
std::cout << std::fixed << std::setprecision(2);
std::cout << "Demonstração de Transformação Afim\n";
std::cout << "---------------------------------\n\n";
// Definição da matriz A, vetor b e vetor x
Eigen::MatrixXd A(2, 2);
A << 2.0, 1.0,
0.0, 3.0;
Eigen::Vector2d b(3.0, 1.0);
Eigen::Vector2d x(1.0, 2.0);
std::cout << "Matriz A =\n" << A << "\n\n";
std::cout << "Vetor b =\n" << b << "\n\n";
std::cout << "Vetor x =\n" << x << "\n\n";
try {
Eigen::VectorXd result = affineTransform(A, b, x);
std::cout << "T(x) = Ax + b =\n" << result << "\n";
} catch (const std::exception& e) {
std::cerr << "Erro: " << e.what() << "\n";
}
return 0;
}
6. O Fim do Começo
Fizemos um passeio longo: vetores e seus espaços, o produto escalar como medida de similaridade, a multiplicação de matrizes como máquina de transformar representações, as derivadas que tornam o aprendizado possível e as transformações afins que estruturam cada camada. Nada disso foi apresentado por acaso. A fórmula da atenção, $\text{softmax}\left(QK^\top/\sqrt{d_k}\right)V$, que parecia um hieróglifo algumas seções atrás, é apenas uma composição das operações que a atenta leitora acabou de dominar: transformações afins criam $Q$, $K$ e $V$; uma multiplicação de matrizes compara todas as queries com todas as keys; e as derivadas dessas operações permitem ajustar os pesos, um gradiente de cada vez.
No próximo artigo sairemos da matemática pura e entraremos na linguagem: veremos como transformar textos em vetores, começando pela representação mais simples e intuitiva, a frequência de termos, até chegarmos à Similaridade de Cosseno prometida na Seção 2.3. Até lá, fica a ideia que esta série levará adiante: toda a arquitetura dos Transformers é uma coreografia de multiplicações de matrizes; quem domina a dança das dimensões, domina o modelo.
Referências
GOLUB, G. H.; VAN LOAN, C. F. Matrix Computations. 4. ed. Baltimore: Johns Hopkins University Press, 2013.
-
VASWANI, Ashish et al. Attention is all you need. In: ADVANCES IN NEURAL INFORMATION PROCESSING SYSTEMS, 30., 2017, Long Beach. Proceedings of the […]. Red Hook: Curran Associates, Inc., 2017. p. 5998-6008. Disponível em: https://papers.nips.cc/paper_files/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf. Acesso em: 09 fevereiro 2024. ↩
Índice da Série: Transformers
- 1. Você Pensa Como Fala
- 2. A Temida Matemática (Você está aqui)
- 3. A Vetorização Básica
- 4. Redes Neurais Artificiais para Word Embedding
- 5. Embeddings Distribuídos e CBoW
- 6. SkipGram e Otimizações do Word2Vec
- 7. Word2Vec, a Ponte para o Contexto
- 8. Desvendando a Modelagem de Sequências
- 9. Prestando Atenção
- 10. Do Código à Geração
- 11. O Cisma e a Batalha da Eficiência
- 12. GEMM o Coração Matemático
(Updated: )