Pull to refresh

Язык твой — друг твой. Дообучаем языковые модели, собираем корпуса, делаем книги на малых языках

Level of difficultyMedium
Reading time14 min
Views6.6K
Матерый языковой энтузиаст
Матерый языковой энтузиаст

Всем привет. Хочу поделиться с сообществом небольшим опытом и наработками для исследования и развития языков, в особенности малых. Для большинства таких языков нет ни систем машинного перевода, ни виртуальных ассистентов, ни других языковых моделей. Основная проблема тут в недостатке данных и отсутствии большого интереса у крупных компаний в их развитии. Однако есть достаточно большое число людей, которым дорог их язык, и которые прикладывают усилия по его сохранению и развитию. Предлагаю обсудить это и познакомиться с инструментами, которые помогут не только собирать данные, но и делать на их основе полезные вещи, типа параллельных книг для изучения языка и систем машинного перевода.

Мы научимся:

  1. Дообучать мультиязычные языковые модели, переводящие текст в векторное представление (эмбеддинги).

  2. Использовать их для выравнивания текстов библиотекой lingtrain-aligner, извлекая из текстов параллельные корпуса.

  3. Загружать датасеты и модели на HuggingFace, чтобы это было доступно всем.

  4. Создавать из выравнивания параллельные книги для изучения языков.

  5. Начнем собирать датасет инструкций на малых языках, чтобы языковые модели и виртуальные смогли понимать и общаться на чувашском, якутском, башкирском и других языках.

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

Глава 1. Обучаем LaBSE марийскому языку

Модель LaBSE — это языковая модель, которая переведет фразы типа «как дела?» и «how are you?» в векторы чисел типа (0.43.., 2.71..., 1.56.., ...) — эмбеддинги, причем чем ближе фразы будут по смыслу, тем меньше будет расстояние между векторами. Языков эта модель поддерживает более ста, передавать их в качестве параметров не нужно. Вот их список:

https://arxiv.org/pdf/2007.01852.pdf

Есть ряд других подобных моделей, но LaBSE в целом является наиболее универсальной и качественной. Для оценки encoder-моделей есть тест MTEB и его рейтинг, оценку мультиязычных моделей можно посмотреть на вкладке Bitext Mining.

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

Список языков LaBSE довольно широк, особенно радует наличие разных языковых семей, это нам очень поможет. Однако, если мы хотим начать использовать её для малых языков, например, марийского, мокшанского или ингушского, то получим довольно низкое качество. Это качество будет варьироваться в зависимости от наличия родственного языка и объема данных при обучении модели. Башкирский язык будет кодироваться более-менее качественно благодаря наличию татарского, но их обоих можно улучшить при дообучении.

Для начала, как ни странно, нам понадобится параллельный корпус. Откуда его взять, если мы только начинаем работу с новым языком? Тут не обойтись без помощи языковых энтузиастов. На старте придется потратить некоторые усилия для сбора такого корпуса вручную. Это не так страшно как может показаться на первый взгляд. Существует большое количество уже сформированных языковых сообществ, в которых люди собирают параллельные и моно-корпуса чувашского, марийских, башкирского и других языков. Есть и просто отдельные люди, которые вручную составили параллельный корпус на несколько тысяч пар предложений, например, на ингушском. Призываю вас поделиться в комментариями такими примерами, это позволит объединить наши усилия по поддержке малых языков.

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

from collections import defaultdict
from datasets import Dataset, DatasetDict
from glob import glob
from tqdm import tqdm


def make_pairs(lang1, lang2, path1, path2):
    data_1 = open(path1).read().splitlines()
    data_2 = open(path2).read().splitlines()
    print(f'Lengths: len(data_1):{len(data_1)}, len(data_2):{len(data_2)}')

    train_items = defaultdict(list)
    for item_1, item_2 in zip(data_1, data_2):
        train_items[lang1].append(clean(item_1))
        train_items[lang2].append(clean(item_2))
    return train_items

#чистим тексты, если нужно  
def clean(text):
    text = text.replace(u'\xa0', ' ')
    return text

def make_dataset(items):
    ds = DatasetDict({
        'train': Dataset.from_dict(items)
    })
    return ds

#токен возьмите в настройках профиля на HF
def upload_dataset(ds, name):
    ds.push_to_hub(name, token='you_hf_token') 
#код языка в новом датасете
lang1 = 'sah'
lang2 = 'ru'

#входные тексты
path1 = './parall_ykt.txt'
path2 = './parall_ru.txt'

pairs = make_pairs(lang1, lang2, path1, path2)
dataset = make_dataset(pairs)

#ваш логин и название датасета
#если хотите загрузить датасет в проект lingtrain, но напишите мне в tg:@averkij
upload_dataset(dataset, name='lingtrain/yakut-russian')

Как вы, наверное, заметили из заголовка, мы возьмем русско-марийский параллельный корпус. Отмечу здесь Андрея Чемышева, усилиями которого этот корпус прирастает новыми единицами, также Андрей очень помог с поиском редакций «Маленького принца» на малых языках России, об этом ниже.

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

Загрузим датасет:

LANG_1 = 'mhr'
LANG_2 = 'ru'

DATASET_LANG_1 = 'mhr'
DATASET_LANG_2 = 'rus'

ds = load_dataset('AigizK/mari-russian-parallel-corpora', split='train')

def prepare_dataset(ds, feature_1, feature_2):
    res = []
    for item in ds:
        res.append({LANG_1:item[feature_1], LANG_2:item[feature_2]})
    return res
        
train_dataset_orig = prepare_dataset(ds,
                                    feature_1=DATASET_LANG_1,
                                    feature_2=DATASET_LANG_2)

В подготовленном датасете будут содержаться вот такие примеры:

{'mhr': 'Мардеж толашыме годым мыйын вуйышкем икте почеш весе, неле шонымаш-шамыч толын пурат.',
 'ru': 'В голове, под шум бури, поднимались и летели одна за другой тяжелые мысли.'}

На этом датасете будет происходить обучение LaBSE, но проверять качество мы должны на паре текстов, которых в корпусе нет. Иначе у нас будет утечка данных, то есть хорошее качество на тестах, но неизвестное при реальном использовании.

Подготовим такие тексты (artamonov_mhr.txt и artamonov_ru.txt) и создадим специальный объект для проверки качества ChainScoreEvaluator:

db_path = f"alignment_{LANG_1}.db"
evaluation_steps = 100

text1_input = "mari/artamonov_mhr.txt"
text2_input = "mari/artamonov_ru.txt"

with open(text1_input, "r", encoding="utf8") as input1:
    text1 = input1.readlines()
with open(text2_input, "r", encoding="utf8") as input2:
    text2 = input2.readlines()
    
evaluator = ChainScoreEvaluator(db_path,
                                  LANG_1,
                                  LANG_2,
                                  text1,
                                  text2,
                                  model=model,
                                  evaluation_steps=evaluation_steps
                                )

Полный код обучения будет по ссылке ниже, проверка качества идет через библиотеку lingtrain_aligner, о ней будет рассказано подробней.

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

Дообучать проще всего через обертку от авторов библиотеки sentence-transformers. В ней есть все необходимые компоненты в виде целевых функций и кода для дообучения.

Вызов выглядит примерно так:

num_epochs = 1
train_batch_size = 8
warmup_steps = 1000

...
train_dataloader = DataLoader(train_dataset, shuffle=True, batch_size=train_batch_size)
train_loss = losses.MultipleNegativesRankingLoss(model=model)

model.fit(train_objectives=[(train_dataloader, train_loss)],
          evaluator=evaluator,
          epochs=num_epochs,
          evaluation_steps=evaluation_steps,
          output_path=model_save_path,
          save_best_model=False,
          use_amp=True,
          warmup_steps=warmup_steps,
          scheduler = 'warmupcosine',
          optimizer_params = {'lr': 1e-5}, # default 2e-5 
)

Параметры надо попробовать разные, главное понимать, что мы тут делаем. А делаем мы следующее — подаем модели на вход пары предложений и заставляем её приближать эмбеддинги для соответствующих пар и отдалять от всех остальных. Поэтому батч тут должен содержать по несколько примеров. Причем не факт, что большой батч будет увеличивать качество. Все зависит от ваших данных, попробуйте размер от 6 до 10.

На каждом шаге проверки (evaluation_steps) будет запускаться наш ChainScoreEvaluator, который создаст базу данных для выравнивания, разобьет тексты по предложениям, посчитает текущей моделью эмбеддинги, алгоритмически их сопоставит и разрешит простейшие конфликты выравнивания (один из трех этапов). Подробнее про алгоритм и библиотеку можно почитать здесь. Под конец будет нарисована визуализация выравнивания и история подсчета chain_score метрики.

Выглядит это примерно так:

Визуализация промежуточного выравнивания в конце обучения
Визуализация промежуточного выравнивания в конце обучения
Метрика chain_score
Метрика chain_score

Видим, что метрика отражает наличие связных кусков в нашей тестовой паре, то есть модель научилась лучше сопоставлять предложения по смыслу на марийском и русском языках.

Вот то же самое выравнивание в начале обучения:

Визуализация промежуточного выравнивания в начале обучения
Визуализация промежуточного выравнивания в начале обучения

Лучшая по метрике модель сохраняется в папке ./output/best_model_{lang1}_{lang2}. Можно загрузить её на HuggingFace:

from huggingface_hub import HfApi


api = HfApi(token="your_hf_token")

api.create_repo(
    repo_id="lingtrain/labse-mari", #ваше пространство на HF и название датасета
    repo_type='model',
    exist_ok=True
)

api.upload_folder(
    repo_id="lingtrain/labse-mari",
    folder_path="./output/best_model_mhr_ru",
    repo_type="model",
)

После этого модель станет доступна нам на следующих шагах.

Код

Глава 2. Извлекаем параллельный корпус новой моделью

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

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

Поможет в этом мой пет-проект в виде python библиотеки lingtrain-aligner. Подробнее о нем можно почитать тут и тут. А сейчас я расскажу о там как им пользоваться. Библиотека умеет сохранять информацию об авторе и названии текста, а также о главах, если проставить для них специальные метки. Кроме того, на основе таких меток мы можем выравнивать тексты по сегментам, давая дополнительные подсказки алгоритму.

Вот наш исходный текст на марийском:

Hidden text

ЯТМАН ВАТЕ-ВЛАК

Икымше ужаш

1
Таче ояр игече лийшаш. Йырваш шып. Шопке лышташат ок тарване. Кечат чӱчкен, модын-воштыл лектеш, чевер капшым ончыктен, Ятман ялым нарашта ӱдырла сӱсанен ончалеш. «Кынел, айдеме, коҥгаш олто, марий чесым ямдыле», — ойлынеже пуйто. Кечыжат таче ала-молан путырак чот модеш, чодыра вуйым лӱҥгыкта, кавам вӱр семын йошкарта, шкежат утыр йӱла, кечывалымсе гай когарта. Шудат ночко, лупс дене йылгыжеш, йӱр толшаш огыл.

...

2

...

3

...

Проставим метки для глав и проследим, что в тексте нет номеров страниц, сносок и другой посторонней информации. Должно получиться примерно так:

Hidden text

1%%%%%h2.
Таче ояр игече лийшаш. Йырваш шып. Шопке лышташат ок тарване. Кечат чӱчкен, модын-воштыл лектеш, чевер капшым ончыктен, Ятман ялым нарашта ӱдырла сӱсанен ончалеш. «Кынел, айдеме, коҥгаш олто, марий чесым ямдыле», — ойлынеже пуйто. Кечыжат таче ала-молан путырак чот модеш, чодыра вуйым лӱҥгыкта, кавам вӱр семын йошкарта, шкежат утыр йӱла, кечывалымсе гай когарта. Шудат ночко, лупс дене йылгыжеш, йӱр толшаш огыл.

...

2%%%%%h2.

...

3%%%%%h2.

...

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

Прочитаем тексты, разобьем их по предложениям и создадим объект выравнивания (SQLite база данных):

from lingtrain_aligner import preprocessor, splitter, aligner


lang_from = "mhr"
lang_to = "ru"
db_path = "mari_artamonov.db"
text1_input = "mari/artamonov_mhr_full.txt"
text2_input = "mari/artamonov_ru_full.txt"

with open(text1_input, "r", encoding="utf8") as input1:
    text1 = input1.readlines()

with open(text2_input, "r", encoding="utf8") as input2:
    text2 = input2.readlines()

lines1_prepared = preprocessor.mark_paragraphs(lines1_prepared)
lines2_prepared = preprocessor.mark_paragraphs(lines2_prepared)

splitted_from = splitter.split_by_sentences_wrapper(lines1_prepared, lang_from)
splitted_to = splitter.split_by_sentences_wrapper(lines2_prepared, lang_to)

aligner.fill_db(db_path, lang_from, lang_to, splitted_from, splitted_to)

Выравнивание идет по батчам — отрезкам текста по N предложений с нахлестом, чтобы искать соответствия только поблизости а не в отдаленных кусках текста.

Из нового. Сегментация текстов, дополнительно повышающая качество. Мы можем посмотреть на структуру сегментов, которая будет использоваться при выравнивании:

left_segments, right_segments = aligner.calculate_segments(
    db_path, segmentation_marks=[preprocessor.H2]
)

segments_structure = aligner.get_batch_intersected_for_segments_list(
    db_path, left_segments=left_segments, right_segments=right_segments, batch_size=200
)

segments_structure

#[[0, 1], [2, 3], [4, 5], [6, 7, 8], [9], [10, 11, 12], [13, 14, 15]]

В нашем тексте 7 глав, каждая будет выравниваться независимо от другой, в каждой главе от 1 до 3 батчей в зависимости от длины главы.

Запустим выравнивание:

use_segments = True

batch_ids = range(0, 100)

aligner.align_db(
    db_path,
    model_name="_",    
    batch_size=200,
    window=40,    
    batch_ids=batch_ids,    
    save_pic=False,
    embed_batch_size=100,
    normalize_embeddings=True,
    show_progress_bar=False,
    shift=0,
    model=model,
    use_segments=use_segments,
    segmentation_marks=[preprocessor.H2],
)

Важными параметрами тут являются shift и window. Про них можно почитать в этой статье. Их удобно использовать в приложении, о нем ниже.

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

Батч с разрывами после первичного выравнивания
Батч с разрывами после первичного выравнивания

Видно, что есть разрывы, свидетельствующие о том, что есть куски текста на марийском (mhr), которых нет в тексте на русском. Кроме того, сами линии довольно «пушистые», давайте это поправим.

Выполним код, который разрешит конфликты — несоответствия между цельными цепочками, которые мы считаем правильно подобранными парами.

steps = 3
batch_id = -1
for i in range(steps):
    conflicts, rest = resolver.get_all_conflicts(db_path, min_chain_length=2+i, max_conflicts_len=6*(i+1), batch_id=batch_id)
    resolver.get_statistics(conflicts)
    resolver.get_statistics(rest)

    resolver.resolve_all_conflicts(db_path, conflicts, model_name="_", show_logs=False, model=model)

    print("Resolved. Step", i+1)
    print("Score 1:", metrics.chain_score(db_path))
    print("Score 2:", metrics.chain_score(db_path, mode="both"))

    if len(rest) == 0:
        break

conflicts, rest = resolver.get_all_conflicts(db_path, min_chain_length=2, max_conflicts_len=26, batch_id=batch_id)
resolver.get_statistics(conflicts)
resolver.get_statistics(rest)

resolver.resolve_all_conflicts(db_path, conflicts, model_name="_", show_logs=False, model=model)

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

Батч с разрывами после разрешения конфликтов
Батч с разрывами после разрешения конфликтов

ПервичноеС разрывами мы ничего сделать не сможем, тут надо добавлять пропавшие куски в текст. Отрезки без разрывов выглядят так:

Батч без разрывов после разрешения конфликтов
Батч без разрывов после разрешения конфликтов

Все. Теперь можем извлечь пары предложений:

pairs = reader.get_aligned_pair_chains(db_path, min_len=3)

Дополнительно откинем пары, которые сильно отличаются по длине предложений и получим массив кандидатов в параллельный корпус.

[
  ...
  ['Ӱдыр-влак воштыл колтышт, тидын годым чыланат Ольган могырышт ончальыч.',
     'Девчата сдержанно засмеялись, неодобрительно поглядывая на Ольгу.'],
  ['— Шонен муынат? — Тачана Марина веке савырныш.',
   '— Ну, так какие слова придумаем? — повернулась Тачана к Марине.'],
  ['— Муынам.', '— А вот какие.'],
  ['Келша докан... Тый шокто, мый мурем.',
   'Я их давно придумала, да все не говорила.'],
  ['Колыштса, кузерак лектеш.', 'Играй на тот же мотив.'],
  ['Тачана гармоньым шупшыльо, Марина муралтыш:',
   'Тачана снова заиграла, и Марина запела:'],
  ...
]

Теперь нужно привлечь носителя языка и отсмотреть полученные пары. Айгиз Кунафин при работе с башкирским корпусом использовал telegram-бота для валидации, чтобы массово провалидировать кандидаты при помощи большого числа носителей языка.

Код

Глава 3. Улучшаем выравнивание и делаем из него книгу

Как вы заметили на визуализации, не все конфликты удалось разрешить автоматически. Чтобы вручную довести выравнивание до идеала, из которого можно сделать полноценную параллельную книгу, нужно воспользоваться еще одной наработкой — Lingtrain Alignment Studio. Это веб-приложение, в котором можно делать все то же самое, что и с библиотекой, но через интерфейс. Плюс там есть редактор, возможность загрузить и выгрузить внешнее выравнивание в .lt формате. И сделать параллельную книгу с подсветкой соответствий предложений. Выглядит результат так:

Подсветка соответствий, можно использовать для изучения языка
Подсветка соответствий, можно использовать для изучения языка

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

Надеюсь, вам понравится проект и у вас появятся идеи по его развитию и улучшению. Напишите об этом в комментариях.

Запуск с кастомной моделью

Из нового. Появилась возможность подключить внешнюю модель. Например, обученную в начале статьи и размещенную на HuggingFace. Это позволит нам использовать приложение для нового языка, разрешить оставшиеся конфликты, получив еще более полный параллельный корпус, а при желании и книжку.

Сделать это проще всего так. Клонируем репозиторий проекта:

git clone https://github.com/averkij/a-studio.git

Меняем параметр MODEL_NAME в файле .env на вашу модель. Это должна быть SentenceTransformers модель, размещенная на HF (мы проделали это в первой части статьи).

APP_PORT=80
APP_DATA_PATH=./dev/data
APP_IMG_PATH=./dev/img
APP_CACHE_PATH=./dev/models_cache

MODEL_NAME=lingtrain/labse-mari

Ваши данные будут храниться в папке ./dev/data, при перезапуске они не потеряются. При первом запуске процесса выравнивания модель скачается из интернета и сохранится в папке ./dev/models_cache.

Собираем образ:

docker-compose build

Запускаем его:

docker-compose up

Приложение запустится на http://localhost:80. Про интерфейс подробно рассказывается здесь.

Lingtrain Alignment Studio
Lingtrain Alignment Studio

Приложения

По ходу развития проекта появлялись различные идеи. Например, генерация параллельной книги в PDF формате, с обложкой и содержанием. Была довольно муторная работа, но в итоге получилось создать вещи вот в таком виде:

Сгенерированный на основе выравнивания PDF
Сгенерированный на основе выравнивания PDF

Есть возможность добавлять подсветку и даже фонетические подсказки в случае иероглифических языков.

PDF с добавлением подсветки и фонетики
PDF с добавлением подсветки и фонетики

Примеры использования скриптов я описывал в статье DIY. Книги для всех, даром.

Глава 4. «Малый принц» и книжка-трансформер

Отдельно хочу сказать про одно из ответвлений проекта. Как-то пришла идея, что можно сделать не просто параллельную книгу, а многоязычную. Написав дополнительные скрипты по объединению нескольких выравниваний в одно, получилось из кучи редакций популярного художественного произведения сгенерировать книжки на десяти и более языках. А попутно сделать и соответствующие мультиязычные параллельные корпуса.

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

Вот пример того, как выглядит «Маленький принц» на малых языках России:

«Маленький принц» на малых языках
«Маленький принц» на малых языках

Книжку можно почитать здесь — https://averkij.github.io/prince.

Благодаря языковому сообществу удалось собрать редакции этой книги на чувашском, татарском и башкирском, дигорском, эрзянском и мокшанском, марийском и горномарийском, коми, балкарском, якутском и других языках.

Все это я выровнял в нашем приложении и составил книгу. А параллельный корпус на этих языках доступен на HuggingFace — https://huggingface.co/datasets/lingtrain/minor-prince.

Датасет minor prince (пока что на 11-ти малых языках)
Датасет minor prince (пока что на 11-ти малых языках)

Похожую вещь я проделал с «Мастером и Маргаритой» Булгакова:

В редакциях романа на популярных языках недостатка нет, хотя в некоторых из них, например, в найденном польском переводе, не оказалось нескольких кусков, поэтому пришлось его не включать.

Такой подход еще сыграет свою роль при создании датасетов инструкций на редких языках.

Глава 5. Проект SuperMinor

Здесь я хочу анонсировать еще один проект по развитию малых языков, который направлен на сбор и перевод данных для обучения языковых моделей в инструктивном формате. Виртуальные ассистенты, будь то ChatGPT или GigaChat, наиболее качественно работают только с популярными языками, да и то не со всеми. Про малые языки типа якутского или чувашского и говорить не приходится.

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

Интерфейс проекта SuperMinor
Интерфейс проекта SuperMinor

Данные будут дополнительно валидироваться, все будет находиться в открытом доступе. Сейчас портал подготавливается для запуска и после тестирования с первыми языковыми энтузиастами будет запущен для всех желающих. Про это я напишу в наши каналы — Lingtrain и градиент обреченный, в которых рассказываю про проекты, связанные с языками, и про машинное обучение в целом.

Ссылки

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 43: ↑41 and ↓2+43
Comments11

Articles