17 KiB
Zarządzanie dialogiem z wykorzystaniem technik uczenia maszynowego
Uczenie przez wzmacnianie
Zamiast ręcznie implementować zbiór reguł odpowiedzialnych za wyznaczenie akcji, którą powinien podjąć agent będąc w danym stanie, odpowiednią taktykę prowadzenia dialogu można zbudować, wykorzystując techniki uczenia maszynowego.
Obok metod uczenia nadzorowanego, które wykorzystaliśmy do zbudowania modelu NLU, do konstruowania taktyk prowadzenia dialogu wykorzystuje się również _uczenie przez wzmacnianie (ang. reinforcement learning).
W tym ujęciu szukać będziemy funkcji $Q*: S \times A \to R$, która dla stanu dialogu $s \in S$ oraz aktu dialogowego $a \in A$ zwraca nagrodę (ang. _reward) $r \in R$, tj. wartość rzeczywistą pozwalającą ocenić na ile podjęcie akcji $a$ w stanie $s$ jest korzystne.
Założymy również, że poszukiwana funkcja powinna maksymalizować _zwrot (ang. return), tj. skumulowaną nagrodę w toku prowadzonego dialogu, czyli dla tury $t_0$ cel uczenia powinien mieć postać:
$$ \sum_{t=t_0}^{\infty}{\gamma^{t-1}r_t} $$
gdzie:
$t$: tura agenta,
$r_t$: nagroda w turze $t$,
$\gamma \in [0, 1]$: współczynnik dyskontowy (w przypadku agentów dialogowych bliżej $1$ niż $0$, por. np. Rieser i Lemon (2011)).
Agent dialogowy w procesie uczenia przez wzmacnianie wchodzi w interakcję ze _środowiskiem, które dla akcji podejmowanej przez taktykę prowadzenia dialogu zwraca kolejny stan oraz nagrodę powiązaną z wykonaniem tej akcji w bieżącym stanie.
Sposób w jaki informacje pochodzące ze środowiska są wykorzystywane do znalezienia funkcji $Q*$ zależy od wybranej metody uczenia. W przykładzie przestawionym poniżej skorzystamy z algorytmu $DQN$ (Mnih i in., 2013) co oznacza, że:
będziemy aproksymować funkcję $Q*$ siecią neuronową,
wagi sieci będziemy wyznaczać korzystając z metody spadku gradientu.
Przykład
Ze względu na to, że implementacja algorytmu $DQN$ nie została jeszcze przystosowana do wersji 3.0 środowiska ConvLab
skorzystamy z wersji 2.0.
!mkdir -p l10
%cd l10
!git clone --depth 1 https://github.com/thu-coai/ConvLab-2.git
%cd ConvLab-2
!pip install -e .
%cd ../..
Po zainstalowaniu środowiska ConvLab-2
należy zrestartować interpreter Pythona (opcja _Kernel -> Restart w Jupyter).
from convlab2.dialog_agent.agent import PipelineAgent
from convlab2.dialog_agent.env import Environment
from convlab2.dst.rule.multiwoz import RuleDST
from convlab2.policy.rule.multiwoz import RulePolicy
from convlab2.policy.dqn import DQN
from convlab2.policy.rlmodule import Memory
from convlab2.evaluator.multiwoz_eval import MultiWozEvaluator
import logging
logging.disable(logging.DEBUG)
# determinizacja obliczeń
import random
import torch
import numpy as np
np.random.seed(123)
random.seed(123)
torch.manual_seed(123)
Środowisko, z którym agent będzie wchodził w interakcje zawierać będzie symulator użytkownika wykorzystujący taktykę prowadzenia dialogu zbudowaną z wykorzystaniem reguł.
usr_policy = RulePolicy(character='usr')
usr_simulator = PipelineAgent(None, None, usr_policy, None, 'user') # type: ignore
dst = RuleDST()
evaluator = MultiWozEvaluator()
env = Environment(None, usr_simulator, None, dst, evaluator)
Zobaczmy jak w _ConvLab-2 zdefiniowana jest nagroda w klasie Environment
%%script false --no-raise-error
#
# plik convlab2/dialog_agent/env.py
#
class Environment():
# (...)
def step(self, action):
# (...)
if self.evaluator:
if self.evaluator.task_success():
reward = 40
elif self.evaluator.cur_domain and self.evaluator.domain_success(self.evaluator.cur_domain):
reward = 5
else:
reward = -1
else:
reward = self.usr.get_reward()
terminated = self.usr.is_terminated()
return state, reward, terminated
Jak można zauważyć powyżej akcja, która prowadzi do pomyślnego zakończenia zadania uzyskuje nagrodę $40$, akcja która prowadzi do prawidłowego rozpoznania dziedziny uzyskuje nagrodę $5$, natomiast każda inna akcja uzyskuje "karę" $-1$. Taka definicja zwrotu premiuje krótkie dialogi prowadzące do pomyślnego wykonania zadania.
Sieć neuronowa, którą wykorzystamy do aproksymacji funkcji $Q*$ ma następującą architekturę
%%script false --no-raise-error
#
# plik convlab2/policy/rlmodule.py
# klasa EpsilonGreedyPolicy wykorzystywana w DQN
#
class EpsilonGreedyPolicy(nn.Module):
def __init__(self, s_dim, h_dim, a_dim, epsilon_spec={'start': 0.1, 'end': 0.0, 'end_epoch': 200}):
super(EpsilonGreedyPolicy, self).__init__()
self.net = nn.Sequential(nn.Linear(s_dim, h_dim),
nn.ReLU(),
nn.Linear(h_dim, h_dim),
nn.ReLU(),
nn.Linear(h_dim, a_dim))
# (...)
policy = DQN(is_train=True)
Każdy krok procedury uczenia składa się z dwóch etapów:
- Wygenerowania przy użyciu taktyki (metoda
policy.predict
) oraz środowiska (metodaenv.step
) _trajektorii, tj. sekwencji przejść pomiędzy stanami złożonych z krotek postaci:- stanu źródłowego,
- podjętej akcji (aktu systemowego),
- nagrody,
- stanu docelowego,
- znacznika końca dialogu.
# por. ConvLab-2/convlab2/policy/dqn/train.py
def sample(env, policy, batch_size, warm_up):
buff = Memory()
sampled_num = 0
max_trajectory_len = 50
while sampled_num < batch_size:
# rozpoczęcie nowego dialogu
s = env.reset()
for t in range(max_trajectory_len):
try:
# podjęcie akcji przez agenta dialogowego
a = policy.predict(s, warm_up=warm_up)
# odpowiedź środowiska na podjętą akcje
next_s, r, done = env.step(a)
# dodanie krotki do zbioru danych
buff.push(torch.Tensor(policy.vector.state_vectorize(s)).numpy(), # stan źródłowy
policy.vector.action_vectorize(a), # akcja
r, # nagroda
torch.Tensor(policy.vector.state_vectorize(next_s)).numpy(), # stan docelowy
0 if done else 1) # znacznik końca
s = next_s
if done:
break
except:
break
sampled_num += t
return buff
- Wykorzystania wygenerowanych krotek do aktualizacji taktyki.
Funkcja train
realizująca pojedynczy krok uczenia przez wzmacnianie ma następującą postać
def train(env, policy, batch_size, epoch, warm_up):
print(f'epoch: {epoch}')
buff = sample(env, policy, batch_size, warm_up)
policy.update_memory(buff)
policy.update(epoch)
Metoda update
klasy DQN
wykorzystywana do aktualizacji wag ma następującą postać
%%script false --no-raise-error
#
# plik convlab2/policy/dqn/dqn.py
# klasa DQN
#
class DQN(Policy):
# (...)
def update(self, epoch):
total_loss = 0.
for i in range(self.training_iter):
round_loss = 0.
# 1. batch a sample from memory
batch = self.memory.get_batch(batch_size=self.batch_size)
for _ in range(self.training_batch_iter):
# 2. calculate the Q loss
loss = self.calc_q_loss(batch)
# 3. make a optimization step
self.net_optim.zero_grad()
loss.backward()
self.net_optim.step()
round_loss += loss.item()
# (...)
Przebieg procesu uczenia zilustrujemy wykonując 10 iteracji. W każdej iteracji ograniczymy się do 100 przykładów.
epoch = 10
batch_size = 100
train(env, policy, batch_size, 0, warm_up=True)
for i in range(1, epoch):
train(env, policy, batch_size, i, warm_up=False)
Sprawdźmy jakie akty systemowe zwraca taktyka DQN
w odpowiedzi na zmieniający się stan dialogu.
from convlab2.dialog_agent import PipelineAgent
dst.init_session()
agent = PipelineAgent(nlu=None, dst=dst, policy=policy, nlg=None, name='sys')
agent.response([['Inform', 'Hotel', 'Price', 'cheap'], ['Inform', 'Hotel', 'Parking', 'yes']])
agent.response([['Inform', 'Hotel', 'Area', 'north']])
agent.response([['Request', 'Hotel', 'Area', '?']])
agent.response([['Inform', 'Hotel', 'Day', 'tuesday'], ['Inform', 'Hotel', 'People', '2'], ['Inform', 'Hotel', 'Stay', '4']])
Jakość wyuczonego modelu możemy ocenić mierząc tzw. wskaźnik sukcesu (ang. _task success rate), tj. stosunek liczby dialogów zakończonych powodzeniem do liczby wszystkich dialogów.
from convlab2.dialog_agent.session import BiSession
sess = BiSession(agent, usr_simulator, None, evaluator)
dialog_num = 100
task_success_num = 0
max_turn_num = 50
# por. ConvLab-2/convlab2/policy/evaluate.py
for dialog in range(dialog_num):
random.seed(dialog)
np.random.seed(dialog)
torch.manual_seed(dialog)
sess.init_session()
sys_act = []
task_success = 0
for _ in range(max_turn_num):
sys_act, _, finished, _ = sess.next_turn(sys_act)
if finished is True:
task_success = sess.evaluator.task_success()
break
print(f'dialog: {dialog:02} success: {task_success}')
task_success_num += task_success
print('')
print(f'task success rate: {task_success_num/dialog_num:.2f}')
Uwaga: Chcąc uzyskać taktykę o skuteczności porównywalnej z wynikami przedstawionymi na stronie ConvLab-2 trzeba odpowiednio zwiększyć zarówno liczbę iteracji jak i liczbę przykładów generowanych w każdym przyroście. W celu przyśpieszenia procesu uczenia warto zrównoleglić obliczenia, jak pokazano w skrypcie train.py.
Literatura
Rieser, V., Lemon, O., (2011). Reinforcement learning for adaptive dialogue systems: a data-driven methodology for dialogue management and natural language generation. (Theory and Applications of Natural Language Processing). Springer. https://doi.org/10.1007/978-3-642-24942-6
Richard S. Sutton and Andrew G. Barto, (2018). Reinforcement Learning: An Introduction, Second Edition, MIT Press, Cambridge, MA http://incompleteideas.net/book/RLbook2020.pdf
Volodymyr Mnih and Koray Kavukcuoglu and David Silver and Alex Graves and Ioannis Antonoglou and Daan Wierstra and Martin Riedmiller, (2013). Playing Atari with Deep Reinforcement Learning, NIPS Deep Learning Workshop, https://arxiv.org/pdf/1312.5602.pdf