<jupyter_start><jupyter_text>Продвинутое машинное обучение Домашнее задание 3 Третье домашнее задание посвящено достаточно простой, но, надеюсь, интересной задаче, в которой потребуется творчески применить методы сэмплирования. Как и раньше, в качестве решения **ожидается ссылка на jupyter-ноутбук на вашем github (или публичный, или с доступом для snikolenko); ссылку обязательно нужно прислать в виде сданного домашнего задания на портале Академии**. Как всегда, любые комментарии, новые идеи и рассуждения на тему категорически приветствуются. В этом небольшом домашнем задании мы **попробуем улучшить метод Шерлока Холмса**. Как известно, в рассказе *The Adventure of the Dancing Men* великий сыщик расшифровал загадочные письмена, которые выглядели как пляшущие человечки. Пользовался он для этого так называемым частотным методом: смотрел, какие буквы чаще встречаются в зашифрованных текстах, и пытался подставить буквы в соответствии с частотной таблицей: E — самая частая и так далее. В этом задании мы будем разрабатывать более современный и продвинутый вариант такого частотного метода. В качестве корпусов текстов для подсчётов частот можете взять что угодно, но для удобства вот вам “Война и мир” по-русски и по-английски:https://www.dropbox.com/s/k23enjvr3fb40o5/corpora.zip  Часть 1  Задание Реализуйте базовый частотный метод по Шерлоку Холмсу:* подсчитайте частоты букв по корпусам (пунктуацию и капитализацию можно просто опустить, а вот пробелы лучше оставить);* возьмите какие-нибудь тестовые тексты (нужно взять по меньшей мере 2-3 предложения, иначе вряд ли сработает), зашифруйте их посредством случайной перестановки символов;* расшифруйте их таким частотным методом.  Решение  Импорт библиотек<jupyter_code>import os
import re
import random
from collections import Counter, defaultdict
from copy import copy

import numpy as np
import pandas as pd
from nltk import everygrams
from nltk.tokenize import RegexpTokenizer
from tqdm.notebook import tqdm
np.random.seed(4)<jupyter_output><empty_output><jupyter_text>Загрузка данных<jupyter_code>if not os.path.exists("/content/corpora.zip"):
    !wget -q https://www.dropbox.com/s/k23enjvr3fb40o5/corpora.zip
    !unzip -oq corpora.zip
FILES = ["AnnaKarenina.txt", "WarAndPeace.txt"]
corpus = []
for filename in FILES:
    with open(filename, "r") as fin:
        corpus += fin.readlines()
corpus = " ".join(corpus)<jupyter_output><empty_output><jupyter_text>Расшифровка<jupyter_code>ALPHABET = " абвгдежзийклмнопрстуфхцчшщъыьэюя"
def tokenize(text, alphabet=ALPHABET, tokenizer=RegexpTokenizer(r"\w+")):
    text = text.lower()
    # Filter characters not in alphabet:
    text = "".join([c for c in text if c in alphabet])
    return " ".join(tokenizer.tokenize(text))


def get_ngram_freqs(text, n_gram=1):
    if n_gram > 1:
        text = [
            "".join(ngram) for ngram in everygrams(text, min_len=n_gram, max_len=n_gram)
        ]
    freqs = {
        k: v / len(text)
        for k, v in Counter(text).items()
        if v > 0  # remove zeros, because why do we need them?
    }
    return freqs


def generate_mapping(freqs):
    original = list(freqs.keys())
    replacements = np.random.choice(original, replace=False, size=len(freqs))
    mapping = {
        original_char: replacement_char
        for original_char, replacement_char in zip(original, replacements)
    }
    return mapping


def apply_mapping(text, mapping):
    return "".join([mapping.get(c, "ь") for c in text])


def get_reverse_mapping(corpus_freqs, text_freqs):
    """Нахождение ближайшего по частотности символа."""
    corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
    text_freqs_sorted = sorted(text_freqs.items(), key=lambda x: x[1], reverse=True)

    reverse_mapping = {}
    for text_char, text_freq in text_freqs_sorted:
        min_diff = 1.0  # maximum possible frequency
        best_char = None
        for corpus_char, corpus_freq in corpus_freqs_sorted:
            diff = abs(corpus_freq - text_freq)
            if diff < min_diff:
                best_char = corpus_char
                min_diff = diff

        reverse_mapping[text_char] = best_char
        corpus_freqs_sorted = [
            (char, freq) for char, freq in corpus_freqs_sorted if char != best_char
        ]

    return reverse_mapping


def character_accuracy(text1, text2):
    assert len(text1) == len(text2)
    matching_chars = sum((c1 == c2) for c1, c2 in zip(text1, text2))
    return matching_chars / len(text1)
tokenized_corpus = tokenize(corpus)
corpus_freqs = get_ngram_freqs(tokenized_corpus, n_gram=1)
mapping = generate_mapping(corpus_freqs)
text = """
    Наша работа во тьме —\n
    Мы делаем, что умеем,\n
    Мы отдаем, что имеем,\n
    Наша работа – во тьме.\n
    Сомнения стали страстью,\n
    А страсть стала судьбой.\n
    Все остальное — искусство\n
    В безумии быть собой.\n
    \n
    Хочется закрыть глаза. Это нормально. Цветной калейдоскоп, блестки, искрящийся звездный вихрь – красиво, но я знаю, что стоит за этой красотой.\n
    Глубина. Ее называют «дип», но мне кажется, что по-русски слово звучит правильнее. Заменяет красивый ярлычок предупреждением. Глубина! Здесь водятся акулы и спруты. Здесь тихо – и давит, давит, давит бесконечное пространство, которого на самом деле нет.\n
    В общем-то она добрая, глубина. По-своему, конечно. Она принимает любого. Чтобы нырнуть, нужно не много сил. Чтобы достичь дна и вернуться – куда больше. В первую очередь надо помнить – глубина мертва без нас. Надо и верить в нее, и не верить. Иначе настанет день, когда не удастся вынырнуть.
"""
tokenized_text = tokenize(" ".join(text.split("\n")))
encoded_text = apply_mapping(tokenized_text, mapping)
text_freqs = get_ngram_freqs(encoded_text)
tokenized_text
encoded_text
reverse_mapping = get_reverse_mapping(corpus_freqs, text_freqs)
decoded_text = apply_mapping(encoded_text, reverse_mapping)
decoded_text
character_accuracy(tokenized_text, decoded_text)<jupyter_output><empty_output><jupyter_text>Текст нечитаем, посимвольная точность расшифровки оставляет желать много лучшего.  Часть 2  Задание Вряд ли в результате получилась такая уж хорошая расшифровка, разве что если вы брали в качестве тестовых данных целые рассказы. Но и Шерлок Холмс был не так уж прост: после буквы E, которая действительно выделяется частотой, дальше он анализировал уже конкретные слова и пытался угадать, какими они могли бы быть. Я не знаю, как запрограммировать такой интуитивный анализ, так что давайте просто сделаем следующий логический шаг:* подсчитайте частоты *биграмм* (т.е. пар последовательных букв) по корпусам;* проведите тестирование аналогично п. 1, но при помощи биграмм. В качестве естественной метрики качества можно взять долю правильно расшифрованных букв или, если хочется совсем математически изощриться, расстояние между двумя перестановками, правильной и полученной из модели; но, честно говоря, в этом задании следить за численными метриками не так уж обязательно, будет и глазами всё видно.  Решение<jupyter_code>corpus_freqs_bigram = get_ngram_freqs(tokenized_corpus, n_gram=2)
text_freqs_bigram = get_ngram_freqs(encoded_text, n_gram=2)

corpus_freqs_sorted = sorted(
    corpus_freqs_bigram.items(), key=lambda x: x[1], reverse=True
)
text_freqs_sorted = sorted(text_freqs_bigram.items(), key=lambda x: x[1], reverse=True)
def get_reverse_mapping_ngram(corpus_freqs, text_freqs, n_gram=1):
    corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
    text_freqs_sorted = sorted(text_freqs.items(), key=lambda x: x[1], reverse=True)

    # Iterate over n-grams starting from the most frequent. On each iteration
    # we take into account already decoded symbols:
    reverse_mapping = {}
    for i, (text_ngram, text_freq) in enumerate(text_freqs_sorted):
        filtered_freqs = copy(corpus_freqs_sorted)

        for j in range(n_gram):
            if text_ngram[j] in reverse_mapping:
                filtered_freqs = [
                    (ngram, freq)
                    for ngram, freq in filtered_freqs
                    if ngram[j] == reverse_mapping[text_ngram[j]]
                ]

        min_diff = 1.0  # maximum possible frequency
        best_ngram = None
        for ngram, freq in filtered_freqs:
            diff = abs(freq - text_freq)
            if diff < min_diff:
                best_ngram = ngram
                min_diff = diff

        for j in range(n_gram):
            if text_ngram[j] not in reverse_mapping:
                reverse_mapping[text_ngram[j]] = best_ngram[j]

    return reverse_mapping
reverse_mapping_bigram = get_reverse_mapping_ngram(
    corpus_freqs_bigram, text_freqs_bigram, n_gram=2
)
decoded_text = apply_mapping(encoded_text, reverse_mapping_bigram)
decoded_text
character_accuracy(tokenized_text, decoded_text)<jupyter_output><empty_output><jupyter_text>Качество слегка ухудшилось. Объяснить это можно тем, что биграмм намного больше, чем униграмм, попасть по частоте в правильные — сложнее.  Часть 3  Задание Но и это ещё не всё: биграммы скорее всего тоже далеко не всегда работают. Основная часть задания — в том, как можно их улучшить:* предложите метод обучения перестановки символов в этом задании, основанный на MCMC-сэмплировании, но по-прежнему работающий на основе статистики биграмм;* реализуйте и протестируйте его, убедитесь, что результаты улучшились.  Решение Текст, разбитый на биграммы — это, по сути, марковская цепь. Частота биграмм — вероятность перехода между состояниями цепи.Для обучения перестановки будем считать вероятность порождения именно такого текста как произведение вероятностей всех биграмм, в него входящих.Всего перестановок очень много, поэтому будем использовать жадный алгоритм, основанный на идее MCMC-сэмплирования.Алгоритм:1. Инициализируем перестановки, восстанавливаем текст и вычисляем логарифм правдоподобия $p_{current}$.2. Меняем местами пару символов для перестановки.3. Восстанавливаем текст с новой перестановкой и вычисляем $p_{proposed}$.4. Принимаем новую перестановку с "вероятностью" $\displaystyle p_{accept} = \frac{p_{proposed}}{p_{current}}$.5. Возвращаемся к пункту 2 и повторяем цикл.Мы можем уйти немного не туда и застрять не в том максимуме, поэтому на каждой итерации алгоритма будем делать много попыток и брать в качестве окончательного результата лучшую из них.<jupyter_code>def get_ngram_freqs_smoothed(text, n_gram=2):
    vocab_size = len(set(text)) ** n_gram
    if n_gram > 1:
        text = [
            "".join(ngram) for ngram in everygrams(text, min_len=n_gram, max_len=n_gram)
        ]
    freqs = {
        k: (v + 1) / (len(text) + vocab_size)  # сглаживаем, чтобы не было нулей
        for k, v in Counter(text).items()
    }
    return freqs


def get_text_proba(text, mapping, freqs, n_gram=2, alphabet=ALPHABET):
    decoded_text = apply_mapping(text, mapping)
    log_proba = 0.0
    for i in range(len(decoded_text) - n_gram):
        ngram = decoded_text[i : i + n_gram]
        ngram_proba = freqs.get(
            ngram, 1 / (len(text) + len(alphabet) ** n_gram)
        )  # сглаживаем, чтобы не было нулей
        log_proba += np.log(ngram_proba)
    return log_proba


def get_reverse_mapping_mcmc(
    encoded_text,
    alphabet_encoded,
    alphabet_corpus,
    freqs_corpus,
    n_iters=10000,
    n_trials=10,
    n_gram=2,
):
    accept_count = 0
    best_reverse_mapping = None
    all_mappings = []
    best_log_likelihood = -np.inf

    for trial in tqdm(range(n_trials), leave=False, position=0, total=n_trials):
        alphabet_encoded = list(alphabet_encoded)
        alphabet_iter = list(alphabet_corpus)
        reverse_mapping = {
            k: v
            for k, v in zip(alphabet_encoded, alphabet_iter[: len(alphabet_encoded)])
        }
        log_proba_current = get_text_proba(
            encoded_text, reverse_mapping, freqs_corpus, n_gram=n_gram
        )

        for i in range(n_iters):
            alphabet_proposal = alphabet_iter[:]
            idx1, idx2 = np.random.choice(len(alphabet_proposal), replace=False, size=2)
            alphabet_proposal[idx1], alphabet_proposal[idx2] = (
                alphabet_proposal[idx2],
                alphabet_proposal[idx1],
            )
            reverse_mapping_proposal = {
                k: v
                for k, v in zip(
                    alphabet_encoded, alphabet_proposal[: len(alphabet_encoded)]
                )
            }
            log_proba_proposal = get_text_proba(
                encoded_text, reverse_mapping_proposal, freqs_corpus, n_gram=n_gram
            )

            p_accept = np.exp(log_proba_proposal - log_proba_current)

            if p_accept > np.random.rand():
                accept_count += 1
                alphabet_iter = alphabet_proposal
                log_proba_current = log_proba_proposal
                reverse_mapping = reverse_mapping_proposal

        if log_proba_current > best_log_likelihood:
            best_log_likelihood = log_proba_current
            best_reverse_mapping = reverse_mapping

        all_mappings.append(reverse_mapping)

    print(f"Best likelihood: {best_log_likelihood}")
    print(f"Accept ratio: {accept_count / (n_iters * n_trials)}")
    return best_reverse_mapping
freqs_corpus = get_ngram_freqs_smoothed(tokenized_corpus, n_gram=2)
best_reverse_mapping = get_reverse_mapping_mcmc(
    encoded_text,
    alphabet_encoded=ALPHABET,
    alphabet_corpus=ALPHABET,
    freqs_corpus=freqs_corpus,
)
decoded_text = apply_mapping(encoded_text, best_reverse_mapping)
decoded_text
character_accuracy(tokenized_text, decoded_text)<jupyter_output><empty_output><jupyter_text>Получилось!  Часть 4  Задание Расшифруйте сообщение:`←⇠⇒↟↹↷⇊↹↷↟↤↟↨←↹↝⇛⇯↳⇴⇒⇈↝⇊↾↹↟⇒↟↹⇷⇛⇞↨↟↹↝⇛⇯↳⇴⇒⇈↝⇊↾↹↨←⇌⇠↨↹⇙↹⇸↨⇛↙⇛↹⇠⇛⇛↲⇆←↝↟↞↹⇌⇛↨⇛⇯⇊↾↹⇒←↙⇌⇛↹⇷⇯⇛⇞↟↨⇴↨⇈↹⇠⇌⇛⇯←←↹↷⇠←↙⇛↹↷⇊↹↷⇠←↹⇠↤←⇒⇴⇒↟↹⇷⇯⇴↷↟⇒⇈↝⇛↹↟↹⇷⇛⇒⇙⇞↟↨←↹↳⇴⇌⇠↟↳⇴⇒⇈↝⇊↾↹↲⇴⇒⇒↹⇰⇴↹⇷⇛⇠⇒←↤↝←←↹⇞←↨↷←⇯↨⇛←↹⇰⇴↤⇴↝↟←↹⇌⇙⇯⇠⇴↹↘⇛↨↞↹⇌⇛↝←⇞↝⇛↹↞↹↝↟⇞←↙⇛↹↝←↹⇛↲←⇆⇴⇏`Или это (они одинаковые, второй вариант просто на случай проблем с юникодом):`დჳჵჂႨშႼႨშჂხჂჲდႨსႹႭჾႣჵისႼჰႨჂჵჂႨႲႹႧჲჂႨსႹႭჾႣჵისႼჰႨჲდႩჳჲႨჇႨႠჲႹქႹႨჳႹႹჱჶდსჂႽႨႩႹჲႹႭႼჰႨჵდქႩႹႨႲႭႹႧჂჲႣჲიႨჳႩႹႭდდႨშჳდქႹႨშႼႨშჳდႨჳხდჵႣჵჂႨႲႭႣშჂჵისႹႨჂႨႲႹჵჇႧჂჲდႨჾႣႩჳჂჾႣჵისႼჰႨჱႣჵჵႨეႣႨႲႹჳჵდხსდდႨႧდჲშდႭჲႹდႨეႣხႣსჂდႨႩჇႭჳႣႨႾႹჲႽႨႩႹსდႧსႹႨႽႨსჂႧდქႹႨსდႨႹჱდჶႣნ`  Решение<jupyter_code>message = "←⇠⇒↟↹↷⇊↹↷↟↤↟↨←↹↝⇛⇯↳⇴⇒⇈↝⇊↾↹↟⇒↟↹⇷⇛⇞↨↟↹↝⇛⇯↳⇴⇒⇈↝⇊↾↹↨←⇌⇠↨↹⇙↹⇸↨⇛↙⇛↹⇠⇛⇛↲⇆←↝↟↞↹⇌⇛↨⇛⇯⇊↾↹⇒←↙⇌⇛↹⇷⇯⇛⇞↟↨⇴↨⇈↹⇠⇌⇛⇯←←↹↷⇠←↙⇛↹↷⇊↹↷⇠←↹⇠↤←⇒⇴⇒↟↹⇷⇯⇴↷↟⇒⇈↝⇛↹↟↹⇷⇛⇒⇙⇞↟↨←↹↳⇴⇌⇠↟↳⇴⇒⇈↝⇊↾↹↲⇴⇒⇒↹⇰⇴↹⇷⇛⇠⇒←↤↝←←↹⇞←↨↷←⇯↨⇛←↹⇰⇴↤⇴↝↟←↹⇌⇙⇯⇠⇴↹↘⇛↨↞↹⇌⇛↝←⇞↝⇛↹↞↹↝↟⇞←↙⇛↹↝←↹⇛↲←⇆⇴⇏"
message_freqs = get_ngram_freqs(message, n_gram=1)
corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
message_freqs_sorted = sorted(message_freqs.items(), key=lambda x: x[1], reverse=True)

alphabet_corpus = "".join([c for c, _ in corpus_freqs_sorted])
alphabet_message = "".join([c for c, _ in message_freqs_sorted])
alphabet_corpus, alphabet_message
freqs_corpus = get_ngram_freqs_smoothed(tokenized_corpus, n_gram=2)
best_reverse_mapping = get_reverse_mapping_mcmc(
    message,
    alphabet_encoded=alphabet_message,
    alphabet_corpus=alphabet_corpus,
    freqs_corpus=freqs_corpus,
    n_iters=10000,
    n_trials=50,
)
encoded_message = apply_mapping(message, best_reverse_mapping)
encoded_message<jupyter_output><empty_output><jupyter_text>Тут тоже получилось. :) Посмотрим на посимвольную ошибку:<jupyter_code>original_message = "если вы видите нормальный или почти нормальный текст у этого сообщения который легко прочитать скорее всего вы все сделали правильно и получите максимальный балл за последнее четвертое задание курса хотя конечно я ничего не обещаю"
character_accuracy(original_message, encoded_message)<jupyter_output><empty_output><jupyter_text>Часть 5  Задание *Бонус*: а что если от биграмм перейти к триграммам (тройкам букв) или даже больше? Улучшатся ли результаты? Когда улучшатся, а когда нет? Чтобы ответить на этот вопрос эмпирически, уже может понадобиться погенерировать много тестовых перестановок и последить за метриками, глазами может быть и не видно.  Решение  Зашифрованное сообщение<jupyter_code>corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
message_freqs_sorted = sorted(message_freqs.items(), key=lambda x: x[1], reverse=True)

abc_corpus = "".join([c for c, _ in corpus_freqs_sorted])
abc_message = "".join([c for c, _ in message_freqs_sorted])
abc_corpus, abc_message
freqs_corpus_trigram = get_ngram_freqs_smoothed(tokenized_corpus, n_gram=3)
best_reverse_mapping = get_reverse_mapping_mcmc(
    message,
    alphabet_encoded=alphabet_message,
    alphabet_corpus=alphabet_corpus,
    freqs_corpus=freqs_corpus_trigram,
    n_gram=3,
    n_iters=10000,
    n_trials=20,
)
encoded_message = apply_mapping(message, best_reverse_mapping)
encoded_message
original_message = "если вы видите нормальный или почти нормальный текст у этого сообщения который легко прочитать скорее всего вы все сделали правильно и получите максимальный балл за последнее четвертое задание курса хотя конечно я ничего не обещаю"
character_accuracy(original_message, encoded_message)<jupyter_output><empty_output><jupyter_text>Почти идеально!  Более длинные тексты Сравним качество дешифровки при переходе от биграмм к триграммам на более длинных, чем зашифрованное сообщение, текстах — например, на тестовом примере.<jupyter_code>def test_ngrams(num_tries, n_gram, freqs_corpus, text):
    accuracy_arr = []

    for _ in range(num_tries):
        best_reverse_mapping = get_reverse_mapping_mcmc(
            encoded_text,
            alphabet_encoded=alphabet_text,
            alphabet_corpus=alphabet_corpus,
            freqs_corpus=freqs_corpus,
            n_gram=n_gram,
            n_iters=10000,
            n_trials=5,
        )

        decoded_text = apply_mapping(encoded_text, best_reverse_mapping)
        accuracy_arr.append(character_accuracy(text, decoded_text))

    print(
        f"\nCharacter accuracy: {np.mean(accuracy_arr):.3f}±{np.std(accuracy_arr):.3f}"
    )
mapping = generate_mapping(corpus_freqs)
encoded_text = apply_mapping(tokenized_text, mapping)
text_freqs = get_ngram_freqs(encoded_text)

corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
text_freqs_sorted = sorted(text_freqs.items(), key=lambda x: x[1], reverse=True)

alphabet_corpus = "".join([c for c, _ in corpus_freqs_sorted])
alphabet_text = "".join([c for c, _ in text_freqs_sorted])
alphabet_corpus, alphabet_text
NUM_TRIES = 1
test_ngrams(num_tries=NUM_TRIES, n_gram=2, freqs_corpus=freqs_corpus, text=tokenized_text)
test_ngrams(
    num_tries=NUM_TRIES, n_gram=3, freqs_corpus=freqs_corpus_trigram, text=tokenized_text
)
freqs_corpus_fourgram = get_ngram_freqs_smoothed(tokenized_corpus, n_gram=4)
test_ngrams(
    num_tries=NUM_TRIES, n_gram=4, freqs_corpus=freqs_corpus_fourgram, text=tokenized_text
)<jupyter_output><empty_output><jupyter_text>Качество практически не изменилось.  Более короткие тексты Теперь попробуем наоборот уменьшить размер текста относительно размера зашифрованного сообщения.<jupyter_code>shorter_text = 'короткий текст для проверки качества декодирования последовательностями символов разной длины'

encoded_text = apply_mapping(shorter_text, mapping)
text_freqs = get_ngram_freqs(encoded_text)

corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
text_freqs_sorted = sorted(text_freqs.items(), key=lambda x: x[1], reverse=True)

alphabet_corpus = "".join([c for c, _ in corpus_freqs_sorted])
alphabet_text = "".join([c for c, _ in text_freqs_sorted])
alphabet_corpus, alphabet_text
print(f'Text alphabet to corpus alphabet ratio: ' \
      f'{len(alphabet_text) / len(alphabet_corpus) * 100:.2f}%')
NUM_TRIES = 5
# Биграммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=2, freqs_corpus=freqs_corpus, text=shorter_text)
# Триграммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=3, freqs_corpus=freqs_corpus_trigram, text=shorter_text)
# 4-граммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=4, freqs_corpus=freqs_corpus_fourgram, text=shorter_text)
longer_text = 'немного более длинный текст для проверки качества декодирования последовательностями символов разной длины который также пытается задействовать больше различных символов алфавита'

encoded_text = apply_mapping(longer_text, mapping)
text_freqs = get_ngram_freqs(encoded_text)

corpus_freqs_sorted = sorted(corpus_freqs.items(), key=lambda x: x[1], reverse=True)
text_freqs_sorted = sorted(text_freqs.items(), key=lambda x: x[1], reverse=True)

alphabet_corpus = "".join([c for c, _ in corpus_freqs_sorted])
alphabet_text = "".join([c for c, _ in text_freqs_sorted])
alphabet_corpus, alphabet_text
print(f'Text alphabet to corpus alphabet ratio: ' \
      f'{len(alphabet_text) / len(alphabet_corpus) * 100:.2f}%')
# Биграммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=2, freqs_corpus=freqs_corpus, text=longer_text)
# Триграммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=3, freqs_corpus=freqs_corpus_trigram, text=longer_text)
# 4-граммы:
test_ngrams(num_tries=NUM_TRIES, n_gram=4, freqs_corpus=freqs_corpus_fourgram, text=longer_text)<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup