Извличането е един от най-важните инструменти за бизнес приложения днес. Може да се използва в голямо разнообразие от области като търсене, чатбот, препоръки, откриване на дублиране, класификация, откриване на аномалии и дори извличане на различни езици/модалност на данни.

  • * Имайте предвид, че внедряванията на код в тази статия все още са в процес на работа
  • * Независимо от това, 70% от кода е завършен (предстоящи тестове) и 100% от всичко останало (писмени части) е готово!

По този начин, тази поредица като част от продължението от моята предишна статия, се стреми да проучи някои от текущото състояние на техниката в областта на машинното обучение за отговаряне на въпроси и извличане на информация,в противен случай известен като Dense Retrieval (DR). Включени са и реализации на код.

Конкретно за тази статия се фокусирам върху предтренировъчния етап на DR. Но преди да продължим, ако не сте сигурни какво е Dense Retriever или имате нужда от код на Python, който да се използва заедно с техниките в тази статия, можете да се обърнете към моята уводна статия или предшественик на DR, или тази статия за най-ранната DR техника.

Обединяваща и обща рамка за обучение на Neural Dense Retriever

Предварителен етап и задачи за DR

Днес езиковите модели, базирани на Bert, обикновено се използват за DR, но за съжаление те се представят зле извън кутията. Изследванията сочат използването на външни мерки за сходство, които не работят добре с маскираното езиково моделиране (MLM). Междувременно предварителното обучение за MLM, използвано в PLM, ще обучи тежести на вниманието, които са твърде далеч и недостатъчни за едноетапно изчисляване на подобието. Това доведе до необходимост от 1) задачи преди обучението, обслужвани специално за DR.

Персонализиран етап на групиране за DR

Случайни партиди (по-голям шанс за неинформативни проби) ще обучат DR модели, които се представят по-слабо в сравнение с тези, обучени на 2) конкретно избрани проби в подобна дадена времева рамка. Следователно това наблюдение се основава на съществуващото използване на негативи под формата на тройки (запитване, положително, отрицателно) за DR обучение.

Различни форми на обучение по DR

Прилагането на съществуващи идеи от други области по подобен начин доведе до 3) различни вкусове на процесите на обучение за DR. Такъв напредък позволи на потребителите да компромисират критерии като ефективност, пространство и производителност.

DR валидиране

И накрая, за 4) валидиране на ефективността на DR моделите, изследователите ги тестват върху целия корпус или подмножества от тестовите данни. Такива практики имат присъщо ограничение, тъй като често има разлика между настройките за обучение и реалния свят (в сценарии от реалния свят броят на пасажите, които трябва да се вземат предвид за всяка заявка, е многократно по-голям). Следователно може да се изгради специализиран набор за валидиране, за да се свържат по-добре тестовете и резултатите от реалния свят.

По същество техниките за DR могат да бъдат групирани в 4 етапа. След това ще разгледам проблемите, лежащи в основата на етапа на предварителната подготовка на DR и ще извлека някои уроци, полезни за предварителното обучение на DR.

Етап на предварителна подготовка: проблемите с използването на предварително обучени езикови модели за DR

Съществуващите техники за предварително обучение като MLM са неподходящи за DR

Като цяло техники за предварително обучение като моделиране на маскиран език (MLM) и предвидение на следващо изречение (NSP) се използват в предварително обучени езикови модели (PLM). MLM включва маскиране на дума и след това предсказването й, докато NSP включва класифициране дали две конкатенирани входни изречения (разделени от токен [SEP]) са първоначално съединени (донякъде подобно на DR). Въпреки че тези техники са позволили на езиковите модели (LM) да придобият способността да разбират контекста на пасаж, те все още не успяват да подготвят LM достатъчно добре за DR.

По този начин задачите за предварително обучение трябва да имитират умозаключение

Все пак трябва да се отбележи, че Кръстосаните енкодерикоито кодират два пасажа заедно, за разлика от Би-енкодерите (по-често използвани за първи етап на DR), всъщност се представят добре за DR. Това е така, защото Cross-Encoders се справиха лесно, като разчитаха на задачата NSP, която е много подобна на DR. Това означава, че (A) задачите преди обучението, които имитират DR, са важни за ефективността. Там, където той не успява обаче, е по отношение на неговата неефективност. Например, за да се изчисли матрица на сходство между всяка двойка от заявка и потенциален пасаж с Cross-Encoder, това изисква големи квадратични разходи за изчисление + памет, в допълнение към сложността на вниманието на двойния полином. Да не говорим за по-дългите дължини на въведения текст поради едновременното кодиране на два входни текста.

Нетренираните DR модели ще разчитат на обучени в MLM тежести/поведение

По този начин B) Bi-Encoders, използвани за DR, ще разчитат само на поведение, което са научили по време на задачата за предварително обучение на MLM на ниво дума(което е лошо), а не на NSP.

Обучените в MLM модели се фокусират твърде много върху неподходящи аспекти

Всъщност „Проучване“ на слоевете на вниманието показа, че MLM ги обучава да научават всеки различен аспект от даден език, като част от речта, отношения на зависимост, субект-глагол-обект и т.н. Тези аспекти, за съжаление, се оказаха са неуместни за DR, а по-късно C) изследване показа емпирично, че само семантиката е върховна.

Фината настройка може да помогне да се замени нежеланото предварително обучено поведение

За щастие, както при повечето задачи на невронни мрежи (NN), фината настройка (предварително обучена за MLM без други техники) D) може да помогне за презаписване на поведението на MLM до известна степен. Можем да заключим това, тъй като невронните мрежи са универсални апроксиматори и че задачата за фина настройка често имитира по-добре действителното обучение за DR и изводите в реалния свят. Въпреки че резултатите може да варират поради скритата случайност. Изпълнението също ще има тенденция да не достига нивото на техниката.

Важна е добрата предварителна тренировка

Сега има здрав разум, че фината настройка ще помогне, тъй като Слоевете за внимание, състоящи се от регулируеми тегла, ще се променят, за да отговарят на целевата задача, когато са фини. По-конкретно, в модели, прецизно настроени за задачата DR, [CLS] токените „ще агрегират и ще поставят повече фокус върху семантиката на текста“. Това означава, че E) голям набор от качествени данни и достатъчно време за предварителна подготовка/фина настройка са ключовиза модела в крайна сметка да научи такива тегла на вниманието.

Обучавайте се на данни в домейн, за да избегнете фундаментална слабост на NN

По подобен начин в друго „изследване“ модел, предварително обучен на синтетичен „PAQ набор от данни“ в домейн (от 65 милиона заявки + положителни двойки с изкопани отрицателни, подобно на задача за обучение/извод от реалния свят), постигна добро представяне на набора от данни „NaturalQuestions“ без фина настройка по него. Това наблюдение повтаря точка E), а също и F), при което данните за предварително обучение в домейна са от решаващо значение. За съжаление, f) рядко е осъществима опция в сценарии от реалния свят, тъй като освен данните за обучение, често има липса на големи и висококачествени набори от данни в домейна. Тук могат да помогнат други техники от други етапи, т.е. пакетиране или етап на обучение. Повече за това в следващите статии.

Етап на предварителна подготовка: други тежки проблеми продължават!

Сега, докато повечето от проблемите, пред които е изправено предварителното обучение за DR, са идентифицирани и решени, прехвърлянето на обучение с предварително обучени модели за DR все още не е довело до огромни печалби в производителността,обикновено наблюдавани при повечето други задачи. Този проблем се задълбочава, когато има несъответствие на домейна между предварителното обучение и данните за извод/реалния свят. По този начин DR моделите са досадни за повторно обучение и се поддържат актуализирани при постоянните промени в набор от данни. Освен това са необходими специфични извадки от данни, като твърди отрицателни (повече за това в етапа на групиране) при отсъствието на големи вътрешни и нови набори от данни, както и размери на партиди за обучение. Това усложнява нещата за използване в реалния свят. Въпреки това, има все повече изследвания в тези области.

След това нека прегледаме част от кода, използван за техники за предварително обучение за DR.

Внедряване на съвременни техники за предварителна подготовка

В следващия раздел ще приложа най-съвременните техники за предварително обучение за DR. Започвайки от най-ранната техника през 2019 г. до най-новата, 2022 г. Всяка от техниките е решила някои от проблемите, които идентифицирахме по-рано.

Етап на предварителна подготовка: 1) Задача за обратно затваряне

Inverse Cloze Task (ICT) е една от най-ранните техники за предварително обучение на DR. Това е техника, която много имитира задачата за извод и дори решава проблема с вниманието на MLM до известна степен. По същество ИКТ може да се обясни като такова: при което в пасаж с N изречения, ИКТ произволно взема извадка от изречение, i ~ [1, N] като Заявка. Заявката е съчетана с друго изречение, k != i, което е положителна проба. След това сходството на двойката се увеличава максимално спрямо негативите в партидата: min Sum_i ^ k [ Cross_ent ( Queries, Passages )].

Имайте предвид, че ИКТ като самоконтролирана техника е добре да се използва, ако имате много пасажи освен вашите данни за обучение. ИКТ също дефинира само източника на данни и функцията за загуба. Това означава, че други техники, обхванати в тази статия, могат да се използват заедно с ИКТ, по начин plug-and-play. Ако се интересувате, внедрих ИКТ в моята предишна статия.

И накрая, отвъд ИКТ, „скорошен документ“ предложи идеята за използване на корпус за предварително обучение по ИКТ, който е от същия домейн като вашите данни за обучение, за да се избегнат промени в разпространението, тъй като това е една от слабостите на DR моделите.

2) Кондензатор и кокондензатор

„Кондензаторът“ е техника, въведена през 2020 г., след като авторите разбраха, че теглата на вниманието на PLM са неподходящи за DR. По същество кондензаторът все още използва задачата за предварително обучение на MLM, но направи промени, които помагат да се адаптира поведението на MLM към DR.

По-подробно, кондензаторът направи малки архитектурни промени на модела за DR предварително обучение. По-конкретно, броят на слоевете за внимание в модел, базиран на Bert, е разделен на две групи: Ранни (Уравнение 6 по-долу) и Късни (Уравнение 7). Няколко блока за внимание с множество глави след това се добавят към края на модела, където неговият вход се състои от изходния токен [CLS] от групата Late и неговите жетони за думи от групата Early (Eqn 8).

След това загубата на MLM се използва за трениране на изхода на главата, като по този начин води до подобрение спрямо други DR модели през 2020 г. в два бенчмарка.

В раздела за проблеми казах, че MLM не подготвя добре моделите за DR. Разликата тук, в Condenser, е, че h ^ рано (всички токени представяне на размера: дължина на пасаж * dim) се използва с h_cls ^ late. Това означава, че по-ранните слоеве на вниманието са били виновникът в предварително обучените модели на MLM (тъй като добавянето на h_cls ^ късно помогна за подобряване на производителността). Вероятно предполагайки, че техните индивидуални лексемни думи (h ^ рано) не събират информация от повечето други токени, а вместо това се фокусират само върху няколко токена, може би поради това, че разчитат на научени части от речта или граматически/синтактични модели, което е без значение за DR . Това подсилва нашата първоначална гледна точка относно MLM (относно MLM обучението на PLM за научаване на неприятно поведение, което не е от значение за DR). Това каза, че вече можем да внедрим Condenser като такъв:

# Code to implement a Bert-based model adapted for Condenser pretraining.
import torch, transformers
from torch import nn
from transformers import AutoModel, AutoTokenizer
from torch.cuda.amp import autocast

# need to add MaskedLM head to condenser head

class Condenser(nn.Module):
    """ Class to implement Condenser model """
    def __init__(self, tokenizer: str = 'microsoft/deberta-v3-base',
        model_name: str = 'microsoft/deberta-v3-base', local: bool =False):
        super().__init__()
        self.tokenizer = AutoTokenizer.from_pretrained('microsoft/deberta-v3-base', 
                                        use_fast=True, 
                                        local_files_only=False)
        self.loss_fn = nn.CrossEntropyLoss()
        self.encoder = AutoModel.from_pretrained(model_name, local_files_only=local)
        
        encoder_layer = nn.TransformerEncoderLayer(d_model=768, nhead=12)
        self.condenser_head = torch.nn.TransformerEncoder(encoder_layer, num_layers=4) 
        
        self.MLM_Head = nn.Sequential(
            nn.Linear(768, 768),
            nn.GELU(),
            nn.LayerNorm(768),
            nn.Linear(768, self.tokenizer.vocab_size, bias = False)
        )
     # default forward method
    @autocast()
    def forward(self, input_ids: torch.tensor, attention_mask: torch.tensor) -> torch.tensor:
        output = self.encoder(input_ids=input_ids.to('cuda:0', non_blocking=True), \
                              attention_mask=attention_mask.to('cuda:0', non_blocking=True))
        return output
        
    # forward method for condenser MLM training
    @autocast()
    def forward_condenser(self, input_ids: torch.tensor, attention_mask: torch.tensor, 
                          mask_pos: int = 2, 
                          mask_token_id: int = self.tokenizer.mask_token_id) -> torch.tensor:
        # in this example, the masked position is fixed to always be index 2
        input_ids[:, mask_pos, :] = mask_token_id
        labels = input_ids.clone()
        labels[labels != mask_token_id] = -100
        
        output = self.forward(input_ids.to('cuda:0', non_blocking=True),
                              attention_mask.to('cuda:0', non_blocking=True))
        
        # get early layers' word tokens and late layers' cls
        early_word_tokens = output[5][:, 1:, :] # layer 6, ignore CLS token
        late_cls_token = output[-1][:, 0, :] # last layer, idx 0 
        condenser_input = torch.cat([late_cls_token, late_cls_token], 1) # concat tgt
        
        # get logits of each word w.r.t embedding shape / num words in embedding
        condenser_output = self.condenser_head(
            src=condenser_input.to('cuda:0', non_blocking=True), 
            mask=attention_mask.to('cuda:0', non_blocking=True)
        )
        logits = self.MLM_head(condenser_output[:, 0, :])   
        
        return logits, labels, output[:, 0, :] # shape: bs, input len, vocab_size
    
    # MLM loss function, compares predicted index vs true
    # HOW TO USE: we can provide the logits and labels from function above to get a loss value
    # then loss.backward() to backprop the gradients
    # and optimizer.step() to update the weights by old - lambda * new 
    def compute_mlm_loss(self, logits: torch.tensor, labels: torch.tensor) -> torch.tensor:
        return self.loss_fn(logits.view(-1, logits.size(-1)), \
                            labels.view(-1))
    
model = Condenser().to("cuda:0")

Трябва да се отбележи, че кондензаторната глава е там само за да насочва слоевете внимание към желаното поведение, което е от полза за DR. Следователно, кондензаторната глава не е необходима по време на извода.

След това през 2021 г., с успеха на кондензатора, оригиналните автори се стремят да имитират по-близо задачата за извод на DR и да извлекат по-плавни загуби и градиент чрез увеличаване на размерите на партидите. И в опит да се подобри още повече производителността, беше изобретен наследник на кондензаторната техника.

Наречен CoCondenser, вместо да има само MLM загуба за предварително обучение, авторът добави кръстосана ентропийна загуба, подобна на ICT. При което токенът [CLS] от главата се използва за изчисляване на мярка за сходство. Внесох загубата в моята предишна статия.

Едно допълнително подобрение е използването на Градиентно кеширане, техника, използвана в Metric/Constrative Learning за преодоляване на ограниченията на паметта. Тази техника позволява използването на големи партиди и ключът се крие в разделянето на частичните производни на различни членове и приближаването на крайния градиент. Тази техника може да бъде обяснена накратко чрез:

при което уравнение 8 означава очакването за загуба на Contrastive и MLM. Междувременно уравнение 9 е частта, в която се изпълнява подаването напред без градиране. Тук градиентът на загубата на контраст спрямо всяко [CLS] представяне се изчислява и се съхранява като променлива v_i,j на форма (1 по dim или 768). след това,

уравнение 10 обозначава пълната частична производна на градиента на модела. Тъй като уравнение 10 само по себе си е скъпо за изпълнение и вече имаме първата част от уравнението (квадратно поле от уравнение 9), това ни позволява да приближим крайния градиент уравнение 11 (ние приближаваме тук поради несъответствия във формата, които за съжаление са трудни за разреши ръчно atm за мен).

След това в уравнение 12 можем да добавим MLM градиента за текущата проба към предишния вектор, за да върнем градиента на матричната форма. Можем да приложим градиентно кеширане като такова:

import transformers, time, torch, pandas as pd, numpy as np
from torch import nn
from transformers import AutoModel, AutoTokenizer
from torch.utils.data import Dataset, DataLoader
from torch.cuda.amp import autocast

loss_fn = nn.CrossEntropyLoss()

@autocast()
def compute_loss(a, b):
    targets = torch.arange(0, b.size(0)).to('cuda:0', non_blocking=True)
    sim = torch.mm(a, b.t())
    loss =  loss_fn(sim, targets)
    return loss
    
# Eqn 9)
def forward_no_grad(q, p, batch_size=32):
    """ Implements eqn 9 of Cocondenser.
    Performs dL/dCLS, forward propagation without computation graph.
    Computes grad of loss w.r.t [CLS] reps.
    Args:
      q: cls rep of queries
      p: cls rep of passages
    Returns:
      cls grad of q and p, as well as loss
    """
    
    model.eval()
    q_rep, p_rep = [], []
    # encode the tokenized [CLS] token queries and passages without grad
    # the [:, 0, :] is used to extract only the CLS token
    with torch.no_grad():
        for idx in range(0, q['input_ids'].size(0), batch_size):
            q_rep.append(
                model(q['input_ids'][idx:idx+batch_size],
                      q['attention_mask'][idx:idx+batch_size])[:, 0, :]
            )
            p_rep.append(
                model(p['input_ids'][idx:idx+batch_size],
                      p['attention_mask'][idx:idx+batch_size])[:, 0, :]
            )
        
        q_rep = torch.cat(q_rep, 0)
        p_rep = torch.cat(p_rep, 0)
    
    # now we can compute the grad of the constrastive loss w.r.t the [CLS] representation
    q_rep.requires_grad_()
    p_rep.requires_grad_()
    
    loss = compute_loss(q_rep, p_rep)
    
    # grad of L / Rep
    q_cls_grad, p_cls_grad = torch.autograd.grad(
        outputs = scaler.scale(loss),
        inputs = (q_rep, p_rep),
        retain_graph = False,
        create_graph = False
    ) 
    
    loss.detach()
    q_rep.detach()
    p_rep.detach()
    del q_rep, p_rep

    return q_cls_grad, p_cls_grad, loss

# Eqn 11)
def forward_backward(q, p, q_cls_grad, p_cls_grad, model, batch_size):
    model.train()
    
    with torch.enable_grad():
      # loop over tokenized q/p in sub batches and feed them into model
      for idx in range(0, q['input_ids'].size(0), batch_size):
          q_rep = model(q['input_ids'][idx:idx+batch_size],
                        q['attention_mask'][idx:idx+batch_size])
          
          # compute a approximate value for gradient computation
          grad_q = q_rep.flatten() @ q_cls_grad[idx:idx+batch_size].flatten()
          del q_rep
          
          p_rep = model(p['input_ids'][idx:idx+batch_size],
                        p['attention_mask'][idx:idx+batch_size])
          grad_p = p_rep.flatten() @ p_cls_grad[idx:idx+batch_size].flatten()
          del p_rep

          # THIS IS WHERE WE APPROXIMATE EQN 11
          ((grad_q + grad_p)/2).backward()
          # get MLM loss
          
          
def gc(q, p, model, scaler, batch_size=32):
    model.eval()
    # get grad of loss w.r.t rep
    q_cls_grad, p_cls_grad, loss = forward_no_grad(q, p, batch_size)
    
    # backward grad of rep w.r.t model params
    forward_backward(q, p, q_cls_grad, p_cls_grad, model, batch_size)
    
    # to implement 
    # Eqn 12 second term

    return loss

train()

Забележете, че в нашата реализация за уравнение 11 във функцията forward_backward градиентът ∂h_i,j/∂theta се изчислява чрез точков продукт на CLS представянето и неговия градиент. Това е само приближение на истинския градиент. След това можем да използваме градиентно кеширане в нашия тренировъчен цикъл като такъв:

def train():
    print("************* Started training *************")
    model = Condenser()
  
    optimizer = torch.optim.AdamW(model.parameters(), lr=0.001)
    OUTPUT_PATH = './GC.pt'
    
    count = 0
    for epoch in range(1, 40):
        model.train()
        lap = time.time()
        
        # q and p is tokenized query and passages, you can refer to my
        # previous article to get the necessary code
        for (q, p) in dataloader: 
            with torch.cuda.amp.autocast():
                optimizer.zero_grad()
                
                # THIS IS WHERE WE USED
                # Gradient caching! gc function
                loss = gc(x1, x2, model, scaler, batch_size = 32)

                scaler.step(optimizer)
                scaler.update()
                count += 1

            if count % 10 == 0:
                print("    Batch:",count,"|", "Loss:", loss, "| Took:", round(time.time() - lap), "seconds")
                lap = time.time()

            if count % 300 == 0:
                torch.save(model.state_dict(), OUTPUT_PATH)

            if loss < 1:
                print("Training complete. TOTAL:", count)
                return

Следващата, последната техника, която ще разгледам, е използването на архитектура за автоматично кодиране за предварително обучение.

3) Маскирано автоматично кодиране/декодиране (CoT-MAE)

Въведена най-скоро през 2022 г., тази техника се фокусира върху обучението на слоевете на вниманието да се фокусират само върху събирането и извличането на семантична информация за пасаж.

По-конкретно, Автоматичният енкодер за контекстна маска (CoT-MAE) включва метод, който комбинира ICT, MLM и загуба на реконструкция чрез декодер със слабо измерени параметри. По този начин тези масиви от техники помогнаха на CoT-MAE да постигне най-добра производителност в 3 публични набора от данни (към ноември 2022 г.). Комбинацията от техники може да бъде описана на следното изображение:

при което в (a) обучаващите двойки от заявка и положителна двойка, означени като T_A и T_B (синя и зелена кутия), се вземат проби по начин, подобен на ICT. Малка разлика тук е, че може да има малки празнини между съседните проходи.

В (b), T_A и T_B са маскирани с произволна вероятност съответно от 45 и 15%, преди да бъдат въведени в енкодера/декодера. Енкодерът, който приема T_A, се стреми да минимизира загубата на MLM. Кодираният T_A също се предава на декодера като контекст и декодерът ще го използва с кодиран T_B, за да извлече загуба въз основа на това колко добре декодерът реконструира T_B. Въпреки това можем да внедрим CoT-MAE като такъв:

class CotMae(Condenser):
    def __init__(self):
        super().__init__()
        decoder_layer = nn.TransformerDecoderLayer(d_model=768, nhead=12)
        self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=2)
        
    @autocast
    def forward(self, enc_input_ids, enc_attention_mask, 
                      dec_input_ids, dec_attention_mask):
        # get encoder MLM loss
        logits, labels, CLS_emb = super().forward_condenser(enc_input_ids, enc_attention_mask, \
                                                            mask_pos = 2, mask_token_id = super().tokenizer.mask_token_id)

        ENC_MLM_loss = super().compute_mlm_loss(logits, labels)

        # get decoder MLM loss
        dec_labels = dec_input_ids.clone()
        dec_labels[dec_labels != super().tokenizer.mask_token_id] = -100
        dec_emb_ids = super().encoder.embeddings(dec_input_ids)
        dec_input = torch.cat([CLS_emb, dec_emb_ids[:, 1:]], 1) # replace CLS with the encoders'

        output = self.decoder(tgt=dec_input, tgt_mask=dec_attention_mask)
        dec_logits = super().MLM_head(output[:, 0, :])
        DEC_MLM_loss = super().compute_mlm_loss(dec_logits, dec_labels)
        
        return ENC_MLM_loss, DEC_MLM_loss

## WIP

След обучението ще използваме само Encoder. Освен това забележете, че просто използвам повторно моя код от Condenser от 2). Това е стратегия, която обикновено се използва в много документи за машинно обучение, където съществуващите идеи се комбинират и с малко усукване тук и там се ражда нова техника.

И като оставим това настрана, внедрих общо 3 най-съвременни (през 2019, 2020/2021, 2022) техники за предварителна подготовка за DR.

Заключение и какво следва

В първата част на тази статия обясних проблемите, пред които е изправено Dense Retrieval, по-специално неподходящостта на базираните на Bert модели за DR. Тези констатации ни насочиха да се съсредоточим върху езикови модели за предварително обучение за DR. Накрая завърших статията с внедряване на код на 3 ключови изследователски разработки, които преместиха областта на DR.

Както винаги, не се колебайте да задавате въпроси в коментарите или чрез лични бележки. Ако моята статия ви е помогнала по някакъв начин, моля, оставете ръкопляскане. Можете също така да ме следвате за повече подобни статии в бъдеще. В следващата си статия ще говоря за следващия(те) етап(и) от обучението за DR: избор на информативни проби за обучение и повече, ако времето и продължителността позволяват.