Марсель Пруст и Марков расшифровывают тексты службы безопасности Т9

11

Как будто этот вызов может быть больше Pythonesque по духу ... Никаких предварительных знаний о цепях Маркова или методах шифрования не требуется.

Вы - шпион, которому необходимо получить важную информацию от британской службы безопасности M1S. Агенты M1S хорошо знают, что их сигналы Wi-Fi могут быть перехвачены, их уязвимости безопасности в Android / iOS эксплуатируются и т. Д., Поэтому все они используют Nokia 3310 для передачи текстовой информации, которая набирается с помощью автозаполнения T9 .

Ранее вы взламывали телефоны для доставки в спецслужбу и устанавливали клавиатурные шпионы под их великолепными пластиковыми клавиатурами, так что теперь вы получаете последовательности чисел, соответствующие набранным буквам, так что « орел покинул гнездо, предупреждая агентов » становится

84303245304270533808430637802537808430243687

Но ждать! Некоторые последовательности Т9 неоднозначны («6263» может быть «имя», «грива» или «гобой»; чем более неясным, тем более подозрительным он становится!), Что вы делаете? Вы знаете, что единственным вступительным экзаменом, который использует M1S, является подведение итогов шедевра Марселя Пруста «Воспоминание о прошлом» за 15 секунд, поэтому вы хотите выбрать слово, следующее за предыдущим, в соответствии с его частотным распределением во всем chef-d ' Увр из Пруста!

Можете ли вы взломать код и получить то, что может быть оригинальным сообщением?

Принцип Т9

Клавиатуры Nokia 3310, используемые агентами

Механизм автозаполнения T9 может быть описан следующим образом. Он отображает буквенные символы на цифры, как показано на рисунке выше.

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

Расшифровщик T9 получает последовательность цифр и пытается угадать слово, которое можно набрать с помощью этих клавиш. Он может использовать стандартную таблицу частот, но мы идем на один шаг дальше и предсказываем следующее слово, используя цепочку Маркова!

Образец обучения

Корпус является это сильно раздела версию «Воспоминания о прошлом» Пруст ( s/-/ /g, s/['’]s //gи s/[^a-zA-Z ]//g- убираться в заблуждении притяжательным 's!) Первоначально опубликовано на Университете Аделаиды сайта (текст этой работы находится в общественном достоянии в Австралии).

Весь текст должен быть проанализирован как одна строка, как одно длинное предложение, как один длинный вектор слов (в зависимости от того, что более удобно для вашего языка), лишен разрывов строк и разбит на слова в пробелах . (Я не предоставляю файл с одним абзацем, потому что такие инструменты могут быть недовольны инструментами github.)

Как прочитать весь текст одной строкой / предложением? Пример в 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

задача

Учитывая последовательность цифр как число, верните возможную текстовую строку, которую можно набрать, используя соответствующие клавиши T9, используя цепочку вероятностей, чтобы предсказать следующее слово X на основе этого обучающего текста, рассматриваемого как одно длинное предложение.

Если X является первым T9-словом в тексте и существует несколько догадок, выберите одно из них случайным образом, в противном случае выберите единственное возможное.

Для всех последующих T9-слов X (i), которым предшествует слово, уже расшифрованное w (i-1) :

  1. Если слово T9 X можно преобразовать в обычное слово x одним уникальным способом, сделайте это.
  2. Если для X доступно несколько вариантов преобразования , скажем, x1, x2, ... , найдите предыдущее угаданное слово w .
    • Если за w никогда не следует ничего, что отображается в X в оригинальной работе Пруста, выберите любой из возможных x1, x2, ... наугад.
    • Если w X всегда соответствует w x1 в оригинале и нет одновременных xi , которые можно было бы отобразить в X , выберите x1 .
    • Если w X можно преобразовать в w x1 , w x2 , ..., которые можно найти в корпусе, подсчитайте все возможные xi , следующие за w, и сопоставьте их с X в корпусе и выберите xi с вероятностью xi / (x1 + х2 + ...) .

Пример 2а. Если сообщение есть 76630489, где 489может быть guyили ivy(они встречаются в корпусе хотя бы один раз), 7663его можно расшифровать как some(очень вероятное первое слово). Если за someним никогда не следует ничего, что отображается 489в корпусе, то выбирайте guyили ivyслучайным образом с вероятностью 0,5.

Пример 2б. Если сообщение есть 766302277437, где 2277437может быть barrierили carrier, 7663может быть расшифровано как some. Если Пруст всегда использовал some carrierи никогда some barrier, тогда выбирай some carrier.

Пример 2с. Предположим, что вы хотите расшифровать последовательность 536307663. 5363было предсказано как lend. 7663может быть любой из них: pond, roofи some. Вы подсчитываете вхождения слова, следующего lendза образцом в корпусе. Предположим, вы получили что-то вроде этого (просто для иллюстрации):

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

Так что если 7663ему предшествует lend, есть 7/(7+2+1)=70%вероятность, что 7663означает some, 20% pondи 10% roof. Ваш алгоритм должен выдавать lend someв 70% случаев, lend pondв 20% случаев и т. Д.

Вы можете смело предполагать, что агенты используют только буквы и пробелы z (без знаков препинания, без притяжений 'sи без цифр).

Вы также можете предположить, что агенты M1S никогда не используют какие-либо слова, выходящие за рамки «Воспоминания о прошлом» (это колоссальный словарь из 29 237 слов!).

Функциональность T9 была реализована в этой задаче , так что вы можете взглянуть на нее.

Если вам нужна помощь, вероятностные цепочки были великолепно приручены в этой , той и следующих задачах, но вам даже не нужно знать принцип таких цепочек: все указано в задании.

Контрольные примеры

--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

Правила:

  • Применяются стандартные лазейки .
  • Вы не знаете исходное сообщение, все, что вы получаете, это последовательность цифр и файл proust.txt, который вам просто нужно загрузить в память / рабочую область / что угодно. Нет необходимости иметь что-то автономное; Предположим proust.txt, всегда доступно.
  • Ваш алгоритм должен иметь возможность создавать различные выходные данные с соответствующими вероятностями, если в соответствии с корпусом возможно более одного варианта дешифрования (см. Пример 2c).

Вам нужно быть максимально осторожным, поэтому выигрывает самый короткий код!

PS Очевидное преимущество этого вероятностного алгоритма состоит в том, что вероятность того, что вы получите истинную исходную строку для неоднозначной расшифрованной строки, стремится к одному - просто подождите ...

PPS См. Также Прогноз по частичному сопоставлению .

Андрей Костырка
источник
Замечания Питера Тейлора из песочницы были приняты во внимание. К сожалению, очень мало людей ответили в течение недели, когда он был там опубликован, несмотря на многочисленные обновления, поэтому любые предложения приветствуются! Кстати, это мой первый вызов!
Андрей Костырка
Я подозреваю, что основная причина, по которой вы не получили много ответов, заключается в том, что для понимания этой проблемы необходимы глубокие знания. Если вы хотите , чтобы этот вызов был привлекательным для большей аудитории, я бы рекомендовал включить несколько более ранних примеров, которые показывают цепочку Маркова в действии :)
Натан Меррилл
@NathanMerrill Хорошо, я добавил 3 ссылки на примеры задач. Однако пользователю вообще не нужно знать цепочки Маркова, потому что задача описывается в теле вопроса как можно более алгоритмически: если X, делайте Y с вероятностями, полученными путем вычисления Z в этой обучающей выборке. Я пытался сделать его настолько самодостаточным, насколько это возможно ...
Андрей Костырка,
О, я понимаю. Если бы вы не объяснили это, я бы проголосовал за это. Это просто выглядит , как он нуждается в передовых знаниях :)
Nathan Мерриллы
1
Мне нравится этот вызов, но у меня еще не было времени сесть и создать решение для гольфа. Надеюсь, это скоро произойдет.
Мего

Ответы:

1

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

Во-вторых, нам нужна функция, которая обрабатывает любой текст:

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
}

Тогда мы Т9-ифы Пруст:

p9 <- t9(proust)

Окончательная подготовка: мы разбиваем входную строку на нули, используя функцию, которую мы вызываем prep):

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

А теперь я предлагаю функцию, которая принимает любую входную строку чисел, preps это и расшифровывает слова одно за другим:

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
}

А теперь, что он на самом деле делает:

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" 

Второй пример:

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"

Пожалуйста, не комментируйте, что это можно сыграть в гольф. Кажется, мало кто интересуется этой проблемой из-за моего ужасного многословия, поэтому я опубликовал этот ответ, чтобы показать, как может выглядеть возможная программа. Вам не нужно повышать / понижать этот ответ.

Андрей Костырка
источник
1

Python 3, 316 байт

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=' ')
RootTwo
источник