Aritmética modular en ciencia de datos

es un sistema matemático donde los números cicatrizan después de alcanzar un valor llamado módulo. El sistema a menudo se conoce como “aritmética del reloj” debido a su similitud con la forma en que los relojes analógicos de 12 horas representan el tiempo. Este artículo proporciona una visión general conceptual de la aritmética modular y explora casos de uso práctico en la ciencia de datos.

Descripción conceptual

Lo básico

La aritmética modular define un sistema de operaciones en enteros basado en un entero particular llamado módulo. La expresión x mod d es equivalente al resto obtenido cuando incógnita se divide por d. Si riñonal ≡ x mod dentonces riñonal se dice que es congruente a x mod d. En otras palabras, esto significa que riñonal y incógnita difieren en un múltiplo de do que x – r es divisible por d. El símbolo ‘≡’ (tres líneas horizontales) se usa en lugar de ‘=’ en aritmética modular para enfatizar que estamos lidiando con la congruencia en lugar de la igualdad en el sentido habitual.

Por ejemplo, en Modulo 7, el número 10 es congruente a 3 porque 10 dividido por 7 hojas un resto de 3. Entonces, podemos escribir 3 ≡ 10 moderno 7. En el caso de un reloj de 12 horas, las 2 am son congruentes a las 2 pm (que es 14 moderno 12). En lenguajes de programación como Python, el signo de porcentaje (‘%’) sirve como operador de módulo (por ejemplo, 10 % 7 evaluaría a 3).

Aquí hay un video que explica estos conceptos con más detalle:

https://www.youtube.com/watch?v=-zechldabfo

Resolver congruencias lineales

A congruencia lineal puede ser una expresión modular de la forma norte ⋅ Y ≡ incógnita (mod d), donde el coeficiente norteobjetivo incógnitay módulo d son enteros conocidos y el entero desconocido Y tiene un grado de uno (es decir, no está cuadrado, en cubos, etc.). La expresión 2017 ⋅ Y ≡ 2025 (mod 10000) es un ejemplo de una congruencia lineal; Afirma que cuando 2017 se multiplica por algún entero Yel producto deja un resto de 2025 cuando se divide por 10000. Para resolver Y en la expresión norte ⋅ Y ≡ incógnita (mod d), siga estos pasos:

  1. Encontrar el El mayor divisor común (GCD) del coeficiente norte y módulo dtambién escrito como GCD (norte, d), que es el mayor entero positivo que es un divisor de ambos norte y d. El Algoritmo euclidiano puede usarse para calcular eficientemente el GCD; Esto también producirá un candidato para norte-1el inverso modular del coeficiente norte.
  2. Determinar si existe una solución. Si el objetivo incógnita no es divisible por GCD (norte, d), entonces la ecuación no tiene solución. Esto se debe a que la congruencia solo se puede solucionar cuando el GCD divide el objetivo.
  3. Simplifique la expresión modular, si es necesario, dividiendo el coeficiente norteobjetivo incógnitay módulo d por GCD (norte, d) para reducir el problema a una forma equivalente más simple; Llamemos a estas cantidades simplificadas norte0, incógnita0y d0respectivamente. Esto asegura que norte0 y d0 son coprimo (es decir, 1 es su único divisor común), que es necesario para encontrar un inverso modular.
  4. Calcule el inverso modular norte0-1 de norte0 moderno d0 (Nuevamente, usando el algoritmo euclidiano extendido).
  5. Encuentra una solución para el valor desconocido Y. Para hacer esto, multiplique el inverso modular norte0-1 por el objetivo reducido incógnita0 Para obtener una solución válida para Y moderno d0.
  6. Finalmente, basándose en el resultado del paso 5, genere todas las soluciones posibles. Dado que la ecuación original fue reducida por GCD (norte, d), hay GCD (norte, d) Soluciones distintas. Estas soluciones están separadas uniformemente por el módulo reducido d0y todos son válidos con respecto al módulo original d.

La siguiente es una implementación de Python del procedimiento anterior:

def extended_euclidean_algorithm(a, b):
    """
    Computes the greatest common divisor of positive integers a and b,
    along with coefficients x and y such that: a*x + b*y = gcd(a, b)
    """
    if b == 0:
        return (a, 1, 0)
    else:
        gcd, x_prev, y_prev = extended_euclidean_algorithm(b, a % b)
        x = y_prev
        y = x_prev - (a // b) * y_prev
        return (gcd, x, y)

def solve_linear_congruence(coefficient, target, modulus):
    """
    Solves the linear congruence: coefficient * y ≡ target (mod modulus)
    Returns all integer solutions for y with respect to the modulus.
    """
    # Step 1: Compute the gcd
    gcd, _, _ = extended_euclidean_algorithm(coefficient, modulus)

    # Step 2: Check if a solution exists
    if target % gcd != 0:
        print("No solution exists: target is not divisible by gcd.")
        return None

    # Step 3: Reduce the equation by gcd
    reduced_coefficient = coefficient // gcd
    reduced_target = target // gcd
    reduced_modulus = modulus // gcd

    # Step 4: Find the modular inverse of reduced_coefficient with respect to the reduced_modulus
    _, inverse_reduced, _ = extended_euclidean_algorithm(reduced_coefficient, reduced_modulus)
    inverse_reduced = inverse_reduced % reduced_modulus

    # Step 5: Compute one solution
    base_solution = (inverse_reduced * reduced_target) % reduced_modulus

    # Step 6: Generate all solutions modulo the original modulus
    all_solutions = [(base_solution + i * reduced_modulus) % modulus for i in range(gcd)]

    return all_solutions

Aquí hay algunas pruebas de ejemplo:

solutions = solve_linear_congruence(coefficient=2009, target=2025, modulus=10000)
print(f"Solutions for y: {solutions}")

solutions = solve_linear_congruence(coefficient=20, target=16, modulus=28)
print(f"Solutions for y: {solutions}")

Resultados:

Solutions for y: [225]
Solutions for y: [5, 12, 19, 26]

Este video explica cómo resolver congruencias lineales con más detalle:

https://www.youtube.com/watch?v=5jwbeqy8lvk

Casos de uso de la ciencia de datos

Caso de uso 1: Ingeniería de características

La aritmética modular tiene una serie de casos de uso interesantes en ciencia de datos. Una intuitiva está en el contexto de la ingeniería de características, para codificar características cíclicas como las horas del día. Dado que el tiempo se envuelve cada 24 horas, el tratamiento de horas como valores lineales puede tergiversar relaciones (por ejemplo, 11 pm y 1 am están numéricamente lejos pero temporalmente cerca). Al aplicar la codificación modular (por ejemplo, utilizando transformaciones seno y coseno del Módulo de hora 24), podemos preservar la naturaleza circular del tiempo, permitiendo que los modelos de aprendizaje automático (ML) reconozcan patrones que ocurren durante períodos específicos como la noche. El siguiente código de Python muestra cómo se puede implementar dicha codificación:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Example: List of incident hours (in 24-hour format)
incident_hours = [22, 23, 0, 1, 2]  # 10 PM to 2 AM

# Convert to a DataFrame
df = pd.DataFrame({'hour': incident_hours})

# Encode using sine and cosine transformations
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)

El marco de datos resultante df:

   hour  hour_sin  hour_cos
0    22 -0.500000  0.866025
1    23 -0.258819  0.965926
2     0  0.000000  1.000000
3     1  0.258819  0.965926
4     2  0.500000  0.866025

Observe cómo el uso del seno aún se diferencia entre las horas anteriores y después de las 12 (por ejemplo, codificando 11 pm y 1 am como -0.258819 y 0.258819, respectivamente), mientras que el uso de coseno no (por ejemplo, tanto 11 pm como 1 am se asignan al valor 0.965926). La elección óptima de codificación dependerá del contexto comercial en el que se implementará el modelo ML. En última instancia, la técnica mejora la ingeniería de características para tareas como la detección de anomalías, el pronóstico y la clasificación donde es importante la proximidad temporal.

En las siguientes secciones, consideraremos dos casos de uso de ciencia de datos más grandes de congruencia lineal que implican resolver Y En expresiones modulares de la forma norte ⋅ Y ≡ incógnita (mod d).

Caso de uso 2: Respachado en sistemas de bases de datos distribuidas

En bases de datos distribuidas, los datos a menudo se dividen (o fragmentado) a través de múltiples nodos usando una función hash. Cuando cambia el número de fragmentos, digamos, desde d a d’ – Necesitamos que rehacer Los datos de manera eficiente sin rehacer todo desde cero.

Supongamos que cada elemento de datos se asigna a un fragmento de la siguiente manera:

shard = hash(key) mod d

Al redistribuir elementos a un nuevo conjunto de d’ Fragmentos, es posible que deseemos mapear los viejos índices de fragmentos a los nuevos de una manera que preserva el equilibrio y minimiza el movimiento de datos. Esto puede conducir a la resolución de Y en la expresión norte ⋅ Y ≡ incógnita (mod d), dónde:

  • incógnita es el índice de fragmentos original,
  • d es el viejo número de fragmentos,
  • norte es un factor de escala (o coeficiente de transformación),
  • Y es el nuevo índice de fragmentos que estamos resolviendo

El uso de la aritmética modular en este contexto garantiza un mapeo consistente entre los diseños de fragmentos antiguos y nuevos, minimiza la reasignación, conserva la localidad de datos y permite transformaciones deterministas y reversibles durante la reorganización.

A continuación se muestra una implementación de Python de este escenario:

def extended_euclidean_algorithm(a, b):
    """
    Computes gcd(a, b) and coefficients x, y such that: a*x + b*y = gcd(a, b)
    Used to find modular inverses.
    """
    if b == 0:
        return (a, 1, 0)
    else:
        gcd, x_prev, y_prev = extended_euclidean_algorithm(b, a % b)
        x = y_prev
        y = x_prev - (a // b) * y_prev
        return (gcd, x, y)

def modular_inverse(a, m):
    """
    Returns the modular inverse of a modulo m, if it exists.
    """
    gcd, x, _ = extended_euclidean_algorithm(a, m)
    if gcd != 1:
        return None  # Inverse doesn't exist if a and m are not coprime
    return x % m

def reshard(old_shard_index, old_num_shards, new_num_shards):
    """
    Maps an old shard index to a new one using modular arithmetic.
    
    Solves: n * y ≡ x (mod d)
    Where:
        x = old_shard_index
        d = old_num_shards
        n = new_num_shards
        y = new shard index (to solve for)
    """
    x = old_shard_index
    d = old_num_shards
    n = new_num_shards

    # Step 1: Check if modular inverse of n modulo d exists
    inverse_n = modular_inverse(n, d)
    if inverse_n is None:
        print(f"No modular inverse exists for n = {n} mod d = {d}. Cannot reshard deterministically.")
        return None

    # Step 2: Solve for y using modular inverse
    y = (inverse_n * x) % d
    return y

Prueba de ejemplo:

import hashlib

def custom_hash(key, num_shards):
    hash_bytes = hashlib.sha256(key.encode('utf-8')).digest()
    hash_int = int.from_bytes(hash_bytes, byteorder='big')
    return hash_int % num_shards

# Example usage
old_num_shards = 10
new_num_shards = 7

# Simulate resharding for a few keys
keys = ['user_123', 'item_456', 'session_789']
for key in keys:
    old_shard = custom_hash(key, old_num_shards)
    new_shard = reshard(old_shard, old_num_shards, new_num_shards)
    print(f"Key: {key} | Old Shard: {old_shard} | New Shard: {new_shard}")

Tenga en cuenta que estamos utilizando una función hash personalizada que es determinista con respecto a key y num_shards Para garantizar la reproducibilidad.

Resultados:

Key: user_123 | Old Shard: 9 | New Shard: 7
Key: item_456 | Old Shard: 7 | New Shard: 1
Key: session_789 | Old Shard: 2 | New Shard: 6

Caso de uso 3: privacidad diferencial en el aprendizaje federado

En el aprendizaje federado, los modelos ML están capacitados en dispositivos descentralizados al tiempo que preservan la privacidad del usuario. La privacidad diferencial agrega ruido a las actualizaciones de gradiente para oscurecer las contribuciones individuales entre los dispositivos. A menudo, este ruido se muestrean de una distribución discreta y debe reducirse al módulo para que se ajuste dentro de los rangos limitados.

Supongamos que un cliente envía una actualización incógnitay el servidor aplica una transformación del formulario norte ⋅ (Y + + k) ≡ incógnita (mod d), dónde:

  • incógnita ¿La actualización de gradiente ruidosa se envía al servidor?
  • Y es la actualización de gradiente original (o verdadera),
  • k es el término de ruido (dibujado al azar desde una variedad de enteros),
  • norte es el factor de codificación,
  • d es el módulo (por ejemplo, tamaño del campo finito o rango de cuantización en el que tienen lugar todas las operaciones)

Debido a la naturaleza que presenta la privacidad de esta configuración, el servidor solo puede recuperarse Y + + kla actualización ruidosa, pero no la actualización real Y sí mismo.

A continuación se muestra la configuración de Python ahora familiar:

def extended_euclidean_algorithm(a, b):
    if b == 0:
        return a, 1, 0
    else:
        gcd, x_prev, y_prev = extended_euclidean_algorithm(b, a % b)
        x = y_prev
        y = x_prev - (a // b) * y_prev
        return gcd, x, y

def modular_inverse(a, m):
    gcd, x, _ = extended_euclidean_algorithm(a, m)
    if gcd != 1:
        return None
    return x % m

Prueba de ejemplo simulando a algunos clientes:

import random

# Parameters
d = 97  # modulus (finite field)
noise_scale = 20  # controls magnitude of noise

# Simulated clients
clients = [
    {"id": 1, "y": 12, "n": 17},
    {"id": 2, "y": 23, "n": 29},
    {"id": 3, "y": 34, "n": 41},
]

# Step 1: Clients add noise and mask their gradients
random.seed(10)
for client in clients:
    noise = random.randint(-noise_scale, noise_scale)
    client["noise"] = noise
    noisy_y = client["y"] + noise
    client["x"] = (client["n"] * noisy_y) % d

# Step 2: Server receives x, knows n, and recovers noisy gradients
for client in clients:
    inv_n = modular_inverse(client["n"], d)
    client["y_noisy"] = (client["x"] * inv_n) % d

# Output
print("Client-side masking with noise:")
for client in clients:
    print(f"Client {client['id']}:")
    print(f"  True gradient y       = {client['y']}")
    print(f"  Added noise           = {client['noise']}")
    print(f"  Masked value x        = {client['x']}")
    print(f"  Recovered y + noise   = {client['y_noisy']}")
    print()

Resultados:

Client-side masking with noise:
Client 1:
  True gradient y       = 12
  Added noise           = 16
  Masked value x        = 88
  Recovered y + noise   = 28

Client 2:
  True gradient y       = 23
  Added noise           = -18
  Masked value x        = 48
  Recovered y + noise   = 5

Client 3:
  True gradient y       = 34
  Added noise           = 7
  Masked value x        = 32
  Recovered y + noise   = 41

Observe que el servidor solo puede derivar los gradientes ruidosos en lugar de los originales.

La envoltura

La aritmética modular, con su elegante estructura cíclica, ofrece mucho más que una forma inteligente de decir el tiempo: respalda algunos de los mecanismos más críticos en la ciencia de datos moderna. Al explorar transformaciones modulares y congruencias lineales, hemos visto cómo este marco matemático se convierte en una herramienta poderosa para resolver problemas del mundo real. En casos de uso tan diversos como la ingeniería de características, el resaltado en bases de datos distribuidas y salvaguardar la privacidad del usuario en el aprendizaje federado a través de la privacidad diferencial, la aritmética modular proporciona la abstracción y la precisión necesarias para construir sistemas robustos y escalables. A medida que la ciencia de datos continúa evolucionando, la relevancia de estas técnicas modulares probablemente crecerá, lo que sugiere que a veces, la clave para la innovación radica en el resto.