LINUX.ORG.RU

Как сравнить гистограммы?

 ,


0

1

Захотелось попробовать расшифровать шифр из «Жангады» Жюля Верна. Там был текст из 252 букв: message = 'СГУЧПВЭЛЛЗЙРТЕПНЛНФГИНБОРГЙУГЛЧДКОТХЖГУУМЗДХРЪСГСЮДТПЪАРВЙГГИЩВЧЭЕЦСТУЖВСЕВХАХЯФБЬБЕТФЗСЭФТХЖЗБЗЪГФБЩИХХРИПЖТЗВТЖЙТГОЙБНТФФЕОИХТТЕГИИОКЗПТФЛЕУГСФИПТЬМОФОКСХМГБТЖФЫГУЧОЮНФНШЗГЭЛЛШРУДЕНКОЛГГНСБКССЕУПНФЦЕЕЕГГСЖНОЕЫИОНРСИТКЦЬЕДБУБТЕТЛОТБФЦСБЮЙПМПЗТЖПТУФКДГ' и N-значное число, задававшее кольцевые сдвиги. В романе шифр описывался как принципиально невзламываемый из-за большой вычислительной сложности, но к моменту публикации его уже научились вскрывать. Для этого берут срезы (или какой принят термин?) message[0::N], message[1::N] … message[N-1::N], для каждого строят гистограмму вероятностей букв и сравнивают с эталонной, насколько нужно сдвинуть.

Вопрос: как это сравнение гистограмм реализовать программно?

Я попробовал сравнивать суммы квадратов разностей, перебирая сдвиги для каждой длины ключа, и это позволило найти сдвиги, угадав длину ключа. Но для разных длин ключа сравнивать эти суммы напрямую нельзя — наименьшая сумма вышла для 1-значного ключа. Как их сравнивать?

Для определённости — код:

message = 'СГУЧПВЭЛЛЗЙРТЕПНЛНФГИНБОРГЙУГЛЧДКОТХЖГУУМЗДХРЪСГСЮДТПЪАРВЙГГИЩВЧЭЕЦСТУЖВСЕВХАХЯФБЬБЕТФЗСЭФТХЖЗБЗЪГФБЩИХХРИПЖТЗВТЖЙТГОЙБНТФФЕОИХТТЕГИИОКЗПТФЛЕУГСФИПТЬМОФОКСХМГБТЖФЫГУЧОЮНФНШЗГЭЛЛШРУДЕНКОЛГГНСБКССЕУПНФЦЕЕЕГГСЖНОЕЫИОНРСИТКЦЬЕДБУБТЕТЛОТБФЦСБЮЙПМПЗТЖПТУФКДГ'

alph32 = 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'
def rln32(letter):
    '''Номер буквы 32-буквенного русского алфавита.'''
    return alph32.index(letter);

def letter_frequencies(text):
    '''Частоты букв в тексте.'''
    return [ text.count(letter) / len(text) for letter in alph32 ]

etalon = letter_frequencies(sample_text) # эталонная гистограмма вероятностей букв

def shiftmatch(a1, a2, shift):
    '''Сумма квадратов разностей элементов списков для сдвига shift.'''
    return sum((val - a2[ (pos + shift) % 32 ])**2  for pos, val in enumerate(a1))

for razr in range(1,13): # длина ключа
    shifts = [0] * razr
    for place in range(razr): # позиция в ключе
        probs = letter_frequencies(message[place::razr]) # гистограмма для позиции
        match = [ shiftmatch(etalon, probs, s)*100 for s in range(32) ]
        mm = min(match)
        shifts[place] = match.index(mm);
        print(razr, place, shifts[place], mm, sep = '\t');
    print(''.join(alph32[(rln32(l) - shifts[p % razr]) % 32] for p,l in enumerate(message)))

Для 6-значного ключа скрипт подобрал верное значение [4, 3, 2, 5, 1, 3], но как численно обосновать, что ключ 6-разрядный?

★★★★★

sample_text — текст «Жангады» в переводе Шишмарёвой.

etalon = [0.0844525216383405, 0.01727602585261991, 0.043843624406284254, 0.019483955501240977, 0.032772479306565204, 0.08386982979098687, 0.011534148891940585, 0.017298073652249505, 0.06771194234815366, 0.011499502349665503, 0.03216773965958197, 0.046583850931677016, 0.03270318622201504, 0.06381263149937637, 0.10975079687047233, 0.027392816196943545, 0.04666259307321129, 0.054842326735791765, 0.0598125307094352, 0.028378667808952665, 0.002022098194600179, 0.008267924861098862, 0.004412709611580764, 0.01342081060310181, 0.007483653131417484, 0.0030236982349161555, 0.0001354364834389528, 0.01903040076600355, 0.021849369432930595, 0.002623688155922039, 0.005905660615070616, 0.019975306464414852]
question4 ★★★★★
() автор топика
Ответ на: комментарий от burato

Сижу дома трезвый по причине болезни. В гости 31-го не ходил по причине болезни тех, кто меня обычно приглашает. И это даже не коронавирус.

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 1)

Сумма квадратов разностей это правильная метрика. Но что бы это работало текст должен быть сильно длиньше чем 252 буквы.

Так бы я проверял вхождение полученных при расшифровке с каким нить ключем слов в словарь, это как будет постабильнее.

AntonI ★★★★
()
Ответ на: комментарий от AntonI

Сумма квадратов разностей это правильная метрика. Но что бы это работало текст должен быть сильно длиньше чем 252 буквы.

Это работает даже для 252/6 = 43 и даже для 252/12 = 21-22 букв. Но получаются большие флуктуации и дисперсия. По какому закону она уменьшается с ростом числа слагаемых? Что сделать с метриками для 1-разрядного ключа и для 12-разрядного, чтобы их можно было сравнивать?

Так бы я проверял вхождение полученных при расшифровке с каким нить ключем слов в словарь

Фактически, это я и делаю вручную. Хотя можно задействовать какой-нибудь wordlist или даже hunspell. Проблема в том, что нет пробелов.

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 1)
Ответ на: комментарий от question4

Но получаются большие флуктуации и дисперсия.

То есть не работает. Можно почитать статьи Орлова и Ко из ипм им.келдыша, они статметодами определяли авторство текстов. Это не совсем твоя задача, но близко.

По какому закону она уменьшается с ростом числа слагаемых?

Ошибка ведет себя 1/srt(n) где n число слагаемых. Так что упс.

Что сделать с метриками для 1-разрядного ключа и для 12-разрядного, чтобы их можно было сравнивать?

Как то отнормировать на число слагаемых? Я до конца не понял что именно ты делаешь, но могу предположить что гистограмма для 1разрядного ключа будет на малом числе букоф лучше именно из за меньшей ошибки. Хотя это все мимо кассы.

AntonI ★★★★
()
Ответ на: комментарий от AntonI

Можно почитать статьи Орлова и Ко из ипм им.келдыша, они статметодами определяли авторство текстов.

Если по частоте слов, то это именно то, что нужно. Но там что-то всё сложно для понимания.

1/srt(n)

Что значит srt? sqrt, квадратный корень?

Я до конца не понял что именно ты делаешь

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

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

question4 ★★★★★
() автор топика
Ответ на: комментарий от AntonI

с каким нить ключем слов в словарь

Да, очень хорошо работает, особенно если брать слова длиннее 4 букв.

import py7zr
wordlist = py7zr.SevenZipFile('russian-wordlist-inflections-1251.txt.7z').read()['russian-wordlist-inflections-1251.txt'].read().decode('cp1251').upper().split()

def wl_score(text):
    '''Суммарная длина найденных слов длиннее 4 букв.'''
    return sum( len(w) for w in wordlist if (len(w) >= 4) and (w in text) )

for razr in range(1,13):
    shifts = [0] * razr
    for place in range(razr):
        probs = letter_frequencies(message[place::razr])
        match = [ shiftmatch(etalon, probs, s)*100 for s in range(10) ]
        mm = min(match)
        shifts[place] = match.index(mm)
        print(razr, place, shifts[place], mm, sep = '\t')
    t = ''.join(alph32[(rln32(l) - shifts[p % razr]) % 32] for p,l in enumerate(message));
    print(wl_score(t), t)

В шифрованном тексте находит 1 слово — «РУДЕНКО» :)

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 1)

Попробовал другую метрику — делить разность частот на эталонную частоту и только потом возводить в квадрат.

def shiftmatch(a1, a2, shift):
    return sum( (1 - a2[ (pos + shift) % 32 ]/val)**2  for pos, val in enumerate(a1) )

В результате получился неверный ключ [8, 3, 2, 5, 1, 3], но для 6-значного ключа сумма невязок минимальна — 357, тогда как у остальных от 659, чаще >1000. И в этом случае сравнение со словарём хуже предсказывает лучший вариант, особенно если не отсеивать 1-3 буквенные слова.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

https://library.keldysh.ru/preprint.asp?id=2022-43

Нет, там не частота слов, там все хитрее. Строится гистограмма по двухбуквенным сочетаниям (буквы стоящие рядом), типа доя каждого автора она уникальна.

Что значит srt? sqrt, квадратный корень?

Да, с телефона пишу, очепятка

найти какую-нибудь формулу, в которую можно подставить невязки для двух решений и сказать, которое из них правильнее.

Чем невязка меньше тем лучше?:-)

AntonI ★★★★
()
Ответ на: комментарий от AntonI

Чем невязка меньше тем лучше?:-)

Это очевидно :) Но формул напридумывали много, и какие-то подходят лучше других.

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

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Возводи в квадрат только разность частот в числителе. Эталонную частоту в знаменателе не возводи. Получится что-то вроде статистики хи-квадрат Пирсона. Она работает для случайных выборок, что будет с данной задачей, не знаю.

TeopeTuK ★★★★★
()
Ответ на: комментарий от TeopeTuK

Возводи в квадрат только разность частот в числителе. Эталонную частоту в знаменателе не возводи. Получится что-то вроде статистики хи-квадрат Пирсона.

Точно. Та же самая формула для goodness-of-fit. Спасибо.

Результат тот же, что и для среднего квадратов разностей. Но теперь для 6 максимальный из GoF меньше максимальных для других длин ключей, а минимальный меньше всех.

Раз уж зашла речь о Пирсоне, нельзя ли как-то использовать его критерий для сравнения гипотез о длине ключа и для сравнения гипотез о величине сдвига для данного среза?

question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 1)
Ответ на: комментарий от AntonI

https://library.keldysh.ru/preprint.asp?id=2022-43

Нет, там не частота слов, там все хитрее. Строится гистограмма по двухбуквенным сочетаниям (буквы стоящие рядом), типа доя каждого автора она уникальна.

Спасибо за статью! Для меня она наверное самое интересное во всем этом топике.

praseodim ★★★★★
()

Попробовал посчитать индексы совпадений — сумму квадратов частот для каждой буквы. Заодно вопрос: в каких случаях нужно вычитать 1, а в каких не нужно?

def freq2_1(text):
    '''Индекс совпадений -- сумма квадратов частот'''
    return sum( f*(f-1) for f in (text.count(letter) for letter in alph32) ) / len(text) / (len(text) - 1)
    #return sum( text.count(letter)**2 for letter in alph32 ) / len(text)**2

Для случайного набора из 32 букв она равна 1/32 = 0.03125, для русского языка в Википедии приведено 0.0553, у меня на эталонном тексте получилось 0.0556.

Для шифрованного текста сразу получилось 0.041, а 0.055 появилось уже на одном из слайсов для 3-значного ключа. Но от слайса к слайсу может колебаться от 0.028 до 0.095. Можно ли просто сложить и усреднить? Если да, то для 6-значного ключа получается локальный максимум 0.056.

for razr in range(1, 13):
    print(razr, sum(freq2_1(message[slice::razr]) for slice in range(razr))/razr, sep='\t')
question4 ★★★★★
() автор топика