TechLead
Intermedio
35 min
Guía completa

Procesamiento de Lenguaje Natural

Enseñar a las máquinas a comprender y generar lenguaje humano

¿Qué es el Procesamiento de Lenguaje Natural?

El Procesamiento de Lenguaje Natural (PLN) es una rama de la IA que permite a los computadores entender, interpretar y generar lenguaje humano. Cierra la brecha entre la comunicación humana y la comprensión de las máquinas, impulsando aplicaciones desde chatbots hasta traducción.

🗣️ El desafío:

El lenguaje humano es ambiguo, dependiente del contexto y evoluciona constantemente. Enseñar a las máquinas a entenderlo requiere manejar gramática, semántica, pragmática y matices culturales.

Tareas clave de PLN

📝 Clasificación de texto

Categorizar texto en clases predefinidas.

Ejemplos: detección de spam, análisis de sentimiento, etiquetado de temas

🏷️ Reconocimiento de entidades nombradas (NER)

Identificar y clasificar entidades en el texto.

Ejemplos: personas, lugares, organizaciones, fechas

🌍 Traducción automática

Traducción automática entre idiomas.

Ejemplos: Google Translate, DeepL, chat multilingüe

❓ Preguntas y respuestas

Entender preguntas y proporcionar respuestas.

Ejemplos: chatbots, asistentes virtuales, buscadores

📄 Resumen de texto

Generar resúmenes concisos de textos largos.

Ejemplos: resúmenes de noticias, abstracts, notas de reuniones

✍️ Generación de texto

Crear texto similar al humano a partir de prompts.

Ejemplos: ChatGPT, creación de contenido, generación de código

Pipeline de preprocesamiento de texto

// NLP Text Preprocessing
class TextPreprocessor {
  constructor() {
    this.stopWords = new Set([
      'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at',
      'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was'
    ]);
  }

  // 1. Tokenization: Split text into words
  tokenize(text) {
    return text
      .toLowerCase()
      .replace(/[^a-z0-9s]/g, '') // Remove punctuation
      .split(/s+/)
      .filter(word => word.length > 0);
  }

  // 2. Remove stop words
  removeStopWords(tokens) {
    return tokens.filter(token => !this.stopWords.has(token));
  }

  // 3. Stemming: Reduce words to root form
  stem(word) {
    // Simple suffix removal (Porter Stemmer simplified)
    const suffixes = ['ing', 'ed', 'es', 's', 'ly'];
    for (const suffix of suffixes) {
      if (word.endsWith(suffix)) {
        return word.slice(0, -suffix.length);
      }
    }
    return word;
  }

  // 4. Process entire text
  process(text) {
    const tokens = this.tokenize(text);
    const filtered = this.removeStopWords(tokens);
    const stemmed = filtered.map(token => this.stem(token));
    return stemmed;
  }
}

// Example usage
const preprocessor = new TextPreprocessor();
const text = "Natural Language Processing is revolutionizing how machines understand human language!";

console.log("Original text:", text);
const processed = preprocessor.process(text);
console.log("Processed tokens:", processed);
// Output: ['natur', 'languag', 'process', 'revolut', 'machin', 'understand', 'human', 'languag']

Implementación de análisis de sentimiento

Construyamos un analizador de sentimiento simple:

// Sentiment Analysis Classifier
class SentimentAnalyzer {
  constructor() {
    // Sentiment lexicon (simplified)
    this.positiveWords = new Set([
      'good', 'great', 'excellent', 'amazing', 'wonderful',
      'fantastic', 'love', 'best', 'perfect', 'awesome',
      'happy', 'beautiful', 'brilliant', 'outstanding'
    ]);
    
    this.negativeWords = new Set([
      'bad', 'terrible', 'awful', 'horrible', 'worst',
      'hate', 'poor', 'disappointing', 'useless', 'sad',
      'angry', 'disgusting', 'pathetic', 'boring'
    ]);

    this.intensifiers = new Map([
      ['very', 1.5],
      ['really', 1.5],
      ['extremely', 2.0],
      ['absolutely', 2.0]
    ]);

    this.negations = new Set(['not', 'no', 'never', 'nothing', 'nobody']);
  }

  tokenize(text) {
    return text.toLowerCase()
      .replace(/[^a-zs]/g, '')
      .split(/s+/)
      .filter(word => word.length > 0);
  }

  analyzeSentiment(text) {
    const tokens = this.tokenize(text);
    let score = 0;
    let multiplier = 1;
    let negated = false;

    for (let i = 0; i < tokens.length; i++) {
      const token = tokens[i];

      // Check for intensifiers
      if (this.intensifiers.has(token)) {
        multiplier = this.intensifiers.get(token);
        continue;
      }

      // Check for negations
      if (this.negations.has(token)) {
        negated = true;
        continue;
      }

      // Calculate sentiment
      if (this.positiveWords.has(token)) {
        score += negated ? -1 * multiplier : 1 * multiplier;
      } else if (this.negativeWords.has(token)) {
        score += negated ? 1 * multiplier : -1 * multiplier;
      }

      // Reset modifiers
      multiplier = 1;
      negated = false;
    }

    // Normalize score
    const normalized = Math.max(-1, Math.min(1, score / tokens.length * 2));

    return {
      score: normalized,
      sentiment: normalized > 0.2 ? 'positive' : 
                 normalized < -0.2 ? 'negative' : 'neutral',
      confidence: Math.abs(normalized)
    };
  }
}

// Example usage
const analyzer = new SentimentAnalyzer();

const reviews = [
  "This product is absolutely amazing! I love it!",
  "Terrible experience. Very disappointed.",
  "It's okay, nothing special.",
  "Not bad, but could be better."
];

console.log("Sentiment Analysis Results:
");
reviews.forEach((review, i) => {
  const result = analyzer.analyzeSentiment(review);
  console.log("Review " + (i + 1) + ": "" + review + """);
  console.log("Sentiment: " + result.sentiment + " (score: " + result.score.toFixed(2) + ", confidence: " + result.confidence.toFixed(2) + ")");
  console.log('');
});

🎯 Técnicas clave:

  • Basado en léxico: Usa puntuaciones de sentimiento predefinidas
  • Intensificadores: Palabras como "very" amplifican el sentimiento
  • Manejo de negaciones: "not good" invierte la polaridad
  • Normalización del score: Convierte a escala -1 a +1

Embeddings de palabras: representar palabras como vectores

Los embeddings capturan relaciones semánticas:

// Simple Word2Vec-style embedding (simplified)
class WordEmbedding {
  constructor(embeddingDim = 50) {
    this.embeddingDim = embeddingDim;
    this.vocabulary = new Map();
    this.embeddings = new Map();
  }

  // Initialize random embeddings
  initializeEmbeddings(words) {
    words.forEach(word => {
      if (!this.embeddings.has(word)) {
        const embedding = Array(this.embeddingDim)
          .fill(0)
          .map(() => Math.random() * 2 - 1);
        this.embeddings.set(word, embedding);
        this.vocabulary.set(word, this.vocabulary.size);
      }
    });
  }

  // Get word embedding
  getEmbedding(word) {
    return this.embeddings.get(word) || Array(this.embeddingDim).fill(0);
  }

  // Calculate cosine similarity
  cosineSimilarity(vec1, vec2) {
    const dotProduct = vec1.reduce((sum, val, i) => sum + val * vec2[i], 0);
    const mag1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0));
    const mag2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0));
    return dotProduct / (mag1 * mag2);
  }

  // Find similar words
  findSimilar(word, topK = 5) {
    const wordEmb = this.getEmbedding(word);
    const similarities = [];

    for (const [otherWord, otherEmb] of this.embeddings) {
      if (otherWord !== word) {
        const sim = this.cosineSimilarity(wordEmb, otherEmb);
        similarities.push({ word: otherWord, similarity: sim });
      }
    }

    return similarities
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, topK);
  }
}

// Example
const embedding = new WordEmbedding(100);
const words = ['king', 'queen', 'man', 'woman', 'royal', 'prince', 'princess'];

embedding.initializeEmbeddings(words);
console.log("Word embeddings initialized for:", words);
console.log("
Finding similar words to 'king':");
const similar = embedding.findSimilar('king', 3);
similar.forEach(({ word, similarity }) => {
  console.log("  " + word + ": " + similarity.toFixed(4));
});

// Famous word2vec analogy: king - man + woman ≈ queen
const kingEmb = embedding.getEmbedding('king');
const manEmb = embedding.getEmbedding('man');
const womanEmb = embedding.getEmbedding('woman');

const result = kingEmb.map((v, i) => v - manEmb[i] + womanEmb[i]);
const queenEmb = embedding.getEmbedding('queen');
const similarity = embedding.cosineSimilarity(result, queenEmb);

console.log("
Word analogy test:");
console.log("king - man + woman ≈ queen");
console.log("Similarity to 'queen': " + similarity.toFixed(4));

💡 Los embeddings capturan:

  • • Similitud semántica ("king" cercano a "queen")
  • • Relaciones analógicas (king - man + woman = queen)
  • • Significado contextual (misma palabra, distinto contexto)

Modelos secuencia a secuencia

Arquitectura Seq2Seq para traducción:

RNN codificador

Procesa la entrada
Crea vector de contexto

Vector de contexto

Representación de tamaño fijo
del significado de entrada

RNN decodificador

Genera salida
palabra por palabra

PLN moderno: mecanismo de atención

// Simplified Attention Mechanism
class Attention {
  constructor() {
    this.weights = null;
  }

  // Calculate attention scores
  calculateScores(query, keys) {
    // Dot product attention
    return keys.map(key => {
      return query.reduce((sum, val, i) => sum + val * key[i], 0);
    });
  }

  // Softmax to get attention weights
  softmax(scores) {
    const maxScore = Math.max(...scores);
    const exps = scores.map(s => Math.exp(s - maxScore));
    const sumExps = exps.reduce((a, b) => a + b, 0);
    return exps.map(e => e / sumExps);
  }

  // Apply attention
  forward(query, keys, values) {
    // Calculate attention scores
    const scores = this.calculateScores(query, keys);
    
    // Convert to weights (probabilities)
    this.weights = this.softmax(scores);
    
    // Weighted sum of values
    const output = Array(values[0].length).fill(0);
    for (let i = 0; i < values.length; i++) {
      for (let j = 0; j < values[i].length; j++) {
        output[j] += this.weights[i] * values[i][j];
      }
    }
    
    return output;
  }
}

// Example: Translating "I love AI"
const attention = new Attention();

// Encoder outputs (simplified as random vectors)
const encoderOutputs = [
  [0.5, 0.3, 0.2],  // "I"
  [0.8, 0.6, 0.4],  // "love"
  [0.3, 0.9, 0.7]   // "AI"
];

// Decoder query (what word we're generating)
const decoderQuery = [0.7, 0.5, 0.3];

// Calculate attention
const contextVector = attention.forward(
  decoderQuery,
  encoderOutputs,  // keys
  encoderOutputs   // values
);

console.log("Attention weights:", attention.weights.map(w => w.toFixed(3)));
console.log("Context vector:", contextVector.map(v => v.toFixed(3)));

  
console.log("
Interpretation: The model is paying most attention to:");
attention.weights.forEach((weight, i) => {
  const words = ['I', 'love', 'AI'];
  console.log("  " + words[i] + ": " + (weight * 100).toFixed(1) + "%");
});

💡 Conclusiones clave

  • El PLN conecta el lenguaje humano con las computadoras
  • El preprocesamiento de texto es crucial (tokenización, stemming, stop words)
  • Los embeddings de palabras representan palabras como vectores densos
  • El análisis de sentimiento extrae opiniones del texto
  • La atención ayuda a enfocarse en partes relevantes
  • El PLN moderno usa transformers (ver siguiente tema)