Marcel Proust e Markov decifrano i testi T9 del servizio di sicurezza


11

Come se questa sfida potesse essere più nello spirito di Pythonesque ... Non è richiesta alcuna conoscenza preliminare delle catene di Markov o delle tecniche di crittografia.

Sei una spia che ha bisogno di ottenere alcune informazioni cruciali dal servizio di sicurezza britannico M1S. Gli agenti di M1S sono ben consapevoli che i loro segnali Wi-Fi possono essere intercettati, le loro vulnerabilità di sicurezza Android / iOS sfruttate ecc., Quindi tutti usano Nokia 3310 per trasmettere informazioni di testo che vengono digitate utilizzando il completamento automatico T9 .

In precedenza avevi hackerato i telefoni per essere consegnati all'agenzia di intelligence e avevi installato keylogger sotto le loro gloriose tastiere di plastica, quindi ora ricevi sequenze di numeri corrispondenti alle lettere che hanno digitato, quindi " l'aquila ha lasciato il nido avvisando gli agenti " diventa

84303245304270533808430637802537808430243687

Ma aspetta! Alcune sequenze T9 sono ambigue ("6263" potrebbe essere "nome", "criniera" o "oboe"; più oscuro, più diventa sospetto!), Quindi cosa fai? Sai che l'unico esame di ammissione che M1S utilizza è riassumere il capolavoro di Marcel Proust "Remembrance of Things Past" in 15 secondi, quindi vuoi scegliere la parola che viene dopo quella precedente in base alla sua distribuzione di frequenza nell'intero chef-d ' œuvre of Proust!

Puoi decifrare il codice e ottenere quello che potrebbe essere il messaggio originale?

Il principio di T9

Tastiere Nokia 3310 utilizzate dagli agenti

Il meccanismo di completamento automatico T9 può essere descritto come segue. Associa i caratteri alfabetici ai numeri, come mostrato nella figura sopra.

abc     -> 2
def     -> 3
ghi     -> 4
jkl     -> 5
mno     -> 6
pqrs    -> 7
tuv     -> 8
wxyz    -> 9
<space> -> 0
<other> -> <is deleted>

Il decodificatore T9 riceve una sequenza di cifre e cerca di indovinare la parola che potrebbe essere digitata con quei tasti premuti. Potrebbe usare una tabella di frequenza standard, ma stiamo facendo un ulteriore passo avanti e prevedendo la parola successiva usando una catena di Markov!

Campione di apprendimento

Il corpus è questa versione pesantemente spogliato di “ricerca del tempo perduto” di Proust ( s/-/ /g, s/['’]s //ge s/[^a-zA-Z ]//g- vattene confondendo possessivo 's!), Originariamente pubblicato sul sito dell'Università di Adelaide (Il testo di questo lavoro è nel pubblico dominio in Australia).

L'intero testo deve essere analizzato come una stringa, come una lunga frase, come un lungo vettore di parole (qualunque sia più conveniente per la tua lingua), privato delle interruzioni di riga e diviso in parole negli spazi . (Non fornisco un file a paragrafo singolo perché potrebbe essere disapprovato dagli strumenti github.)

Come posso leggere l'intero testo come una stringa / frase? Un esempio in R :

p_raw  <- read.table("proust.txt", sep="\t") # Because there are no tabs
p_vec  <- as.character(p_raw$V1)       # Conversion to character vector
p_str  <- paste(p_vec, collapse=" ")   # One long string with spaces
p_spl  <- strsplit(p_str, split=" ")[[1]] # Vector of 1360883 words
proust <- p_spl[p_spl!=""]           # Remove empty entries — 1360797

Compito

Data una sequenza di cifre come un numero, restituisce una possibile stringa di testo che potrebbe essere digitata utilizzando i corrispondenti tasti T9 utilizzando una catena di probabilità per prevedere la parola X successiva in base a questo testo di addestramento trattato come una frase lunga.

Se X è la prima parola T9 del testo e ci sono più ipotesi, scegline una a caso, altrimenti scegli l'unica possibile.

Per tutte le successive parole T9 X (i) precedute da una parola già decifrata w (i-1) :

  1. Se una parola T9 X può essere convertita in una parola normale x in un modo unico, fallo.
  2. Se sono disponibili più opzioni di conversione per X , ad esempio x1, x2, ... , cercare la parola indovinata precedente w .
    • Se w non è mai seguito da qualcosa che si associa a X nell'opera originale di Proust, scegli uno qualsiasi dei possibili x1, x2, ... a caso.
    • Se w X corrisponde sempre a w x1 nell'originale e non ci sono xi simultanei che potrebbero essere mappati in X , scegli x1 .
    • Se w X può essere convertito in w x1 , w x2 , ... che può essere trovato nel corpus, allora conta tutti i possibili xi che seguono w e mappano a X nel corpus e scegli xi con probabilità xi / (x1 + x2 + ...) .

Esempio 2a. Se il messaggio è 76630489, dove 489potrebbe essere guyo ivy(si verificano nel corpus almeno una volta), 7663può essere decifrato come some(una prima parola molto probabile). Se somenon è mai seguito da qualcosa che è mappato 489nel corpus, allora scegli guyo ivya caso con probabilità 0,5.

Esempio 2b. Se il messaggio è 766302277437, dove 2277437potrebbe essere barriero carrier, 7663può essere decifrato come some. Se Proust è sempre stato utilizzato some carriere mai some barrier, quindi selezionare some carrier.

Esempio 2c. Supponiamo di voler decifrare la sequenza 536307663. 5363era previsto come lend. 7663potrebbe essere uno di questi: pond, roofe some. Conta le occorrenze della parola che segue lendnel corpus di esempio. Supponiamo di ottenere qualcosa del genere (solo per illustrare):

        T9  Word following lend  Occurrences
      7663  some                           7
      7663  pond                           2
      7663  roof                           1

Quindi, se 7663è preceduto da lend, c'è una 7/(7+2+1)=70%probabilità che 7663rappresenta some, 20% ponde 10% roof. L'algoritmo dovrebbe produrre lend somenel 70% dei casi, lend pondnel 20% dei casi ecc.

Puoi tranquillamente presumere che gli agenti utilizzino solo lettere e spazi az (nessun segno di punteggiatura, nessun carattere possessivo 'se nessun numero).

Si può anche presumere che gli agenti di M1S non usino mai parole al di fuori dell'ambito di “Remembrance of Things Past” (che è un enorme vocabolario di 29.237 parole!).

La funzionalità T9 è stata implementata in questa sfida , quindi potresti dargli un'occhiata.

Se hai bisogno di aiuto, le catene probabilistiche sono state gloriosamente domate in questa , quella e le seguenti sfide, ma non hai nemmeno bisogno di conoscere il principio di tali catene: tutto è dichiarato nel compito.

Casi test

--Inputs--
20784250276960369
20784250276960369
84303245304270533808430637802537808430243687
94280343084306289072908608430262780482737
94280343084306289072908608430262780482737

--Possible outputs--
c quick brown fox
a stick crown fox
the eagle gas left the nest blest vie agents
what did the navy pay to the coast guards
what did the navy raz un the coast guards

Regole:

  • Si applicano scappatoie standard .
  • Non conosci il messaggio originale, tutto ciò che ottieni è una sequenza di cifre e il file proust.txt che devi solo caricare nella memoria / spazio di lavoro / qualunque cosa. Non è necessario avere nulla di indipendente; assumere proust.txtè sempre accessibile.
  • L'algoritmo deve essere in grado di produrre output diversi con le rispettive probabilità se è probabile più di un'opzione di decrittografia in base al corpus (vedere Esempio 2c).

Devi rimanere il più discreto possibile, quindi vince il codice più corto!

PS L'ovvio vantaggio di questo algoritmo probabilistico è il fatto che la probabilità che otterrai una vera stringa originale per una stringa decifrata ambigua tende a una - aspetta solo ...

PPS Vedi anche Pronostico per corrispondenza parziale .


Le osservazioni di Peter Taylor dalla sandbox sono state prese in considerazione. Purtroppo, pochissime persone hanno risposto durante la settimana in cui era stato pubblicato lì nonostante gli aggiornamenti multipli, quindi qualsiasi suggerimento è il benvenuto! A proposito, questa è la mia prima sfida!
Andreï Kostyrka,

Sospetto che un grande motivo per cui non hai ricevuto molte risposte sia a causa delle conoscenze avanzate necessarie per comprendere questo problema. Se si sta vogliono questa sfida per appello a una folla più grande, io consiglierei di cui alcuni esempi precedenti che mostrano la catena di Markov a lavoro :)
Nathan Merrill

@NathanMerrill OK, ho aggiunto 3 collegamenti a esempi di sfide. Tuttavia, un utente non deve conoscere affatto le catene di Markov perché l'attività è descritta nel corpo della domanda nel modo più algoritmico possibile: se X, fai Y con probabilità ottenute calcolando Z in questo esempio di apprendimento. Ho provato a renderlo il più autosufficiente possibile ...
Andreï Kostyrka,

Oh capisco. Se non lo avessi spiegato, avrei votato per chiuderlo. E 'solo sembra che ha bisogno di una conoscenza avanzata :)
Nathan Merrill

1
Mi piace questa sfida, ma non ho ancora avuto il tempo di sedermi e creare / golf una soluzione. Spero che accada presto.
Mego,

Risposte:


1

Soluzione R, illustrazione non competitiva di cosa si può fare

Innanzitutto, cariciamo la sequenza di parole nella memoria:

p_raw  <- read.table("proust.txt", sep="\t") # Because there are no tabs
p_vec  <- as.character(p_raw$V1)       # Conversion to character vector
p_str  <- paste(p_vec, collapse=" ")   # One long string with spaces
p_spl  <- strsplit(p_str, split=" ")[[1]] # Vector of 1360883 words
proust <- p_spl[p_spl!=""]           # Remove empty entries — 1360797

In secondo luogo, abbiamo bisogno di una funzione che T9-ifies qualsiasi testo:

t9 <- function (x) {
  x <- chartr(paste(c(letters, " "), collapse=""), "222333444555666777788899990", tolower(x))
  x <- gsub("[^0-9]", "", x, perl = TRUE) # Safety check
  x <- x[x!=""] # Also for safety because... you know...
  x
}

Quindi, abbiamo T9-ify Proust:

p9 <- t9(proust)

Preparazione finale: abbiamo diviso la stringa di input in zeri usando una funzione che chiamiamo prep):

prep <- function (x) {
  x <- chartr("0", " ", x)
  x <- strsplit(x, " ")[[1]]
  x <- x[x!=""] # Boil the empty strings for safety
  x
}

E ora propongo una funzione che accetta qualsiasi stringa di input di numeri, la preps e decifra le parole una per una:

decip <- function(x, verbose = FALSE) {
  x <- prep(x)
  l <- length(x)
  decrypted <- rep(NA, l)
  tb <- table(proust[which(p9 == x[1])])
  decrypted[1] <- sample(names(tb), 1, prob=tb/sum(tb))
  if (verbose) print(decrypted[1])
  for (i in 2:l) {
    mtchl <- p9 == x[i]
    mtch <- which(mtchl)  # Positions that matched
    pmtch <- proust[mtch] # Words that matched
    tb <- table(pmtch)    # Count occurrences that matched
    if (length(tb)==1) {  # It is either 1 or >1
      decrypted[i] <- names(tb)[1]
      if (verbose) print(paste0("i = ", i, ", case 1: unique decryption"))
      } else {  # If there are more than one ways to decipher...
      preced <- proust[mtch-1] 
      s <- sum(preced==decrypted[i-1])
      if (s==0) {
        decrypted[i] <- sample(names(tb), 1)
        if (verbose) print(paste0("i = ", i, ", case 2a: multiple decryption, collocation never used, picking at random"))
        } else {
        tb2 <- table(pmtch[preced==decrypted[i-1]])
        if (length(tb2)==1) {
          decrypted[i] <-  names(tb2)[1]
          if (verbose) print(paste0("i = ", i, ", case 2b: multiple decryption, only one collocation found, using it"))
        } else {
          decrypted[i] <- sample(names(tb2), 1, prob = tb2/sum(tb2))
          if (verbose) print(paste0("i = ", i, ", case 2c: multiple decryption, ", length(tb2), " choices"))
          }
      }
    }
    if(verbose) print(decrypted[i])
  }
  decrypted
}

E ora cosa sta effettivamente facendo:

decip("20784250276960369", verbose=TRUE)
----
[1] "a"
[1] "i = 2, case 2c: multiple decryption, 2 choices"
[1] "quick"
[1] "i = 3, case 2a: multiple decryption, collocation never used, picking at random"
[1] "brown"
[1] "i = 4, case 1: unique decryption"
[1] "fox"
[1] "a"     "quick" "brown" "fox" 

Secondo esempio:

decip("84303245304270533808430637802537808430243687", verbose=TRUE)
----
[1] "what"
[1] "i = 2, case 2b: multiple decryption, only one collocation found, using it"
[1] "did"
[1] "i = 3, case 2b: multiple decryption, only one collocation found, using it"
[1] "the"
[1] "i = 4, case 1: unique decryption"
[1] "navy"
[1] "i = 5, case 2a: multiple decryption, collocation never used, picking at random"
[1] "raz"
[1] "i = 6, case 2a: multiple decryption, collocation never used, picking at random"
[1] "um"
[1] "i = 7, case 2a: multiple decryption, collocation never used, picking at random"
[1] "the"
[1] "i = 8, case 2b: multiple decryption, only one collocation found, using it"
[1] "coast"
[1] "i = 9, case 1: unique decryption"
[1] "guards"
[1] "what"   "did"    "the"    "navy"   "raz"    "um"     "the"    "coast"  "guards"

Si prega di non commentare che questo può essere giocato a golf. Sembra che poche persone siano interessate a questa sfida a causa della mia terribile verbosità, quindi ho pubblicato questa risposta per mostrare come potrebbe essere un programma possibile. Non è necessario aggiungere / votare questa risposta.


1

Python 3, 316 byte

from random import*
from collections import*
def d(s,f):
 D=defaultdict(Counter);p=q=''
 for w in open(f).read().split():D[w.translate({97+c:(c-(c>17)-(c>24))//3+50for c in range(26)})].update([w]);D[p].update([w]);p=w
 for c in s.split('0'):q=choice([*(len(D[c])>1and D[c]&D[q]or D[c]).elements()]);print(q,end=' ')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.