Sequence-to-One: analiza sentymentu za pomocą LSTM
Uczą się Państwo tworzyć kompleksowy klasyfikator sentymentu: tokenizować recenzje, osadzać słowa za pomocą nn.Embedding, przetwarzać je przez LSTM i klasyfikować końcowy stan ukryty.
Sequence-to-One: analiza sentymentu za pomocą LSTM to bezpłatna lekcja Machine Learning Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Machine Learning Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Machine Learning Academy zawiera 4 lekcji w sumie.
Wyjaśnienie klasyfikacji sequence-to-one
W zadaniach typu sequence-to-one model odczytuje całą sekwencję i generuje pojedynczą etykietę wyjściową. Klasycznym przykładem jest analiza sentymentu: na podstawie pełnej recenzji filmu („Podobała mi się każda minuta tego filmu”) należy przewidzieć jedną etykietę — pozytywną lub negatywną.
Różni się to od zadań typu sequence-to-sequence (tłumaczenie, w którym każdy token wejściowy generuje token wyjściowy) oraz one-to-sequence (tworzenie opisów obrazów). W zadaniu sequence-to-one odrzucamy wyjścia LSTM z pośrednich kroków i używamy tylko końcowego stanu ukrytego h_T, który teoretycznie koduje znaczenie całej sekwencji wejściowej.
Zbiór danych: recenzje filmów IMDB
Zbiór danych IMDB zawiera 50 000 recenzji filmów (25 000 treningowych i 25 000 testowych) oznaczonych jako pozytywne lub negatywne. Każda recenzja jest tekstem o zmiennej długości. Zanim przekażemy ją do LSTM, musimy wykonać trzy kroki wstępnego przetwarzania: tokenizację (podział tekstu na słowa), budowę słownika (przypisanie słowom indeksów całkowitych) oraz dopełnianie (ujednolicenie długości wszystkich sekwencji).
Musimy także obsłużyć bardzo długie recenzje — trenowanie LSTM zwalnia kwadratowo wraz z długością sekwencji, dlatego często obcina się je do 256 lub 512 tokenów bez znacznego pogorszenia dokładności.
from torchtext.datasets import IMDB
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
tokenizer = get_tokenizer('basic_english')
def yield_tokens(data_iter):
for _, text in data_iter:
yield tokenizer(text)
train_iter = IMDB(split='train')
vocab = build_vocab_from_iterator(yield_tokens(train_iter),
specials=['<unk>', '<pad>'])
vocab.set_default_index(vocab['<unk>'])
print('Vocabulary size:', len(vocab))Tokenizacja i dopełnianie sekwencji
Dopełnianie jest konieczne, ponieważ moduły DataLoader w PyTorch wymagają, aby tensory w obrębie jednej partii miały ten sam kształt. Krótsze sekwencje uzupełniamy specjalnym tokenem <pad> (zwykle o indeksie 0), a dłuższe obcinamy do maksymalnej długości.
Najważniejsze jest to, że LSTM powinien ignorować tokeny dopełniające. Narzędzia PyTorch pack_padded_sequence i pad_packed_sequence umożliwiają wydajne pomijanie dopełnienia podczas obliczeń LSTM, co ma znaczenie dla poprawności — w przeciwnym razie model będzie aktualizował stan ukryty na podstawie pozbawionego znaczenia dopełnienia, zniekształcając końcowy stan h_T.
import torch
from torch.nn.utils.rnn import pad_sequence
def text_pipeline(text):
return vocab(tokenizer(text))[:256] # Truncate to 256 tokens
def label_pipeline(label):
return 1 if label == 'pos' else 0
def collate_batch(batch):
labels, texts, lengths = [], [], []
for label, text in batch:
labels.append(label_pipeline(label))
processed = torch.tensor(text_pipeline(text), dtype=torch.long)
texts.append(processed)
lengths.append(len(processed))
texts = pad_sequence(texts, batch_first=True, padding_value=1) # 1 = <pad>
return torch.tensor(labels), texts, torch.tensor(lengths)Warstwa embeddingu: od słów do gęstych wektorów
Całkowitych indeksów słów nie można bezpośrednio przekazać do LSTM — trzeba przekształcić je w wektory ciągłe. Warstwa embeddingu nn.Embedding(vocab_size, embedding_dim) jest tabelą wyszukiwania, która mapuje każdy indeks słowa na gęsty wektor liczb zmiennoprzecinkowych.
Typowe wymiary embeddingów to 100, 200 lub 300. Embeddingi są parametrami, których wartości można uczyć, i są aktualizowane podczas trenowania razem z wagami LSTM. Alternatywnie można zainicjalizować je za pomocą wstępnie wytrenowanych wektorów słów, takich jak GloVe lub Word2Vec, co znacznie przyspiesza trenowanie na małych zbiorach danych dzięki wykorzystaniu wcześniej poznanych zależności semantycznych między słowami.
import torch.nn as nn
vocab_size = 25000
embedding_dim = 100
hidden_size = 128
output_dim = 1 # Binary classification: positive/negative
embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=1)
# Optional: load pre-trained GloVe vectors
# from torchtext.vocab import GloVe
# glove = GloVe(name='6B', dim=100)
# embedding.weight.data.copy_(glove.vectors)
print('Embedding weight shape:', embedding.weight.shape) # (25000, 100)Budowanie modelu SentimentLSTM
Kompletna architektura modelu składa się z trzech części: warstwy embeddingu, LSTM oraz głowicy w pełni połączonej. Embedding przekształca indeksy tokenów w wektory, LSTM przetwarza sekwencję, a warstwa liniowa mapuje końcowy stan ukryty na pojedynczy logit.
Dodajemy dropout za warstwą embeddingu i przed warstwą liniową, aby regularyzować model. Na wyjściu stosujemy sigmoid, aby uzyskać prawdopodobieństwo z zakresu od 0 do 1. Do trenowania używamy funkcji straty Binary Cross-Entropy, ponieważ jest to klasyfikacja binarna.
import torch
import torch.nn as nn
class SentimentLSTM(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_size, num_layers, dropout):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=1)
self.lstm = nn.LSTM(embed_dim, hidden_size, num_layers,
batch_first=True, dropout=dropout)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_size, 1)
def forward(self, text):
embedded = self.dropout(self.embedding(text)) # (B, T, embed_dim)
output, (h_n, c_n) = self.lstm(embedded)
last_hidden = self.dropout(h_n[-1]) # Final layer hidden state
return self.fc(last_hidden).squeeze(1) # (B,) logitsKonfiguracja trenowania: funkcja straty i optymalizator
W przypadku klasyfikacji binarnej używamy nn.BCEWithLogitsLoss, która łączy aktywację sigmoid i binarną entropię krzyżową w jednej numerycznie stabilnej operacji. Jest to rozwiązanie preferowane zamiast ręcznego zastosowania sigmoid, a następnie nn.BCELoss.
Używamy optymalizatora Adam ze współczynnikiem uczenia 1e-3. Adam dostosowuje współczynniki uczenia dla poszczególnych parametrów, dzięki czemu dobrze sprawdza się w przypadku rzadkich gradientów powstających w embeddingach (tylko embeddingi słów występujących w partii otrzymują aktualizacje gradientu). Należy używać przycinania gradientów za pomocą torch.nn.utils.clip_grad_norm_, aby zapobiegać eksplozji gradientów, która jest częstym problemem podczas trenowania sieci RNN.
import torch
import torch.nn as nn
import torch.optim as optim
model = SentimentLSTM(
vocab_size=25000, embed_dim=100,
hidden_size=128, num_layers=2, dropout=0.3
)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCEWithLogitsLoss()
# Gradient clipping prevents exploding gradients in RNNs
MAX_GRAD_NORM = 1.0
print('Model parameters:', sum(p.numel() for p in model.parameters()))Pętla trenowania modelu Sentiment LSTM
Pętla trenowania dla klasyfikacji typu sequence-to-one ma standardową strukturę PyTorch: przebieg w przód, obliczenie funkcji straty, propagacja wsteczna, przycięcie gradientów i aktualizacja parametrów. Obliczamy także dokładność, porównując przewidywaną klasę (wyjście sigmoid > 0.5) z etykietami referencyjnymi.
Ważne jest, aby zarówno model, jak i tensory partii przenieść na to samo urządzenie (CPU lub GPU). Użycie model.to(device) i tensor.to(device) zapewnia spójność obliczeń. Na GPU trenowanie dwuwarstwowego LSTM na zbiorze IMDB zazwyczaj zajmuje od 2 do 5 minut na epokę.
def train_epoch(model, loader, optimizer, criterion, device):
model.train()
total_loss, correct = 0.0, 0
for labels, texts, lengths in loader:
labels, texts = labels.float().to(device), texts.to(device)
optimizer.zero_grad()
predictions = model(texts) # Forward pass
loss = criterion(predictions, labels) # Compute loss
loss.backward() # Backprop
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # Clip
optimizer.step() # Update weights
total_loss += loss.item()
preds = (torch.sigmoid(predictions) > 0.5).float()
correct += (preds == labels).sum().item()
return total_loss / len(loader), correct / len(loader.dataset)Ocena i monitorowanie postępów
Podczas oceny wywołujemy model.eval() i opakowujemy przebiegi w przód w torch.no_grad(), aby wyłączyć dropout i śledzenie gradientów. Dzięki temu uzyskujemy bezstronny obraz skuteczności uogólniania.
W kolejnych epokach śledzimy cztery wartości: stratę treningową, stratę walidacyjną, dokładność treningową i dokładność walidacyjną. Jeśli dokładność treningowa rośnie, ale walidacyjna pozostaje bez zmian, model przeucza się — należy zwiększyć dropout lub zmniejszyć rozmiar modelu. Jeśli obie wartości są niskie, model niedoucza się — należy dodać warstwy LSTM lub zwiększyć rozmiar stanu ukrytego.
def evaluate(model, loader, criterion, device):
model.eval()
total_loss, correct = 0.0, 0
with torch.no_grad():
for labels, texts, lengths in loader:
labels, texts = labels.float().to(device), texts.to(device)
predictions = model(texts)
loss = criterion(predictions, labels)
total_loss += loss.item()
preds = (torch.sigmoid(predictions) > 0.5).float()
correct += (preds == labels).sum().item()
return total_loss / len(loader), correct / len(loader.dataset)
# Training run
for epoch in range(5):
tr_loss, tr_acc = train_epoch(model, train_loader, optimizer, criterion, device)
vl_loss, vl_acc = evaluate(model, val_loader, criterion, device)
print(f'Epoch {epoch+1}: Train Acc={tr_acc:.3f}, Val Acc={vl_acc:.3f}')Oczekiwana skuteczność i typowe błędy
Dobrze dostrojony klasyfikator sentymentu oparty na LSTM zazwyczaj osiąga na zbiorze IMDB dokładność testową na poziomie 85–90%. Do typowych błędów obniżających skuteczność należą: brak ustawienia model.eval() podczas oceny (dropout pozostaje aktywny), brak zerowania gradientów przed propagacją wsteczną oraz trenowanie przez zbyt małą liczbę epok, zanim embeddingi się zbiegną.
Innym problemem jest używanie tego samego indeksu słownika dla nieznanych słów i dopełnienia. Należy rozdzielić tokeny <unk> (indeks 0) i <pad> (indeks 1) oraz przekazać padding_idx=1 do warstwy embeddingu, aby embeddingi dopełnienia zawsze miały wartość zero, a ich gradienty były pomijane podczas trenowania.
# Common mistakes checklist:
# 1. Missing model.eval() before evaluation
# 2. Missing optimizer.zero_grad() before backward
# 3. Applying sigmoid twice (using BCELoss instead of BCEWithLogitsLoss)
# 4. Not handling unknown words (<unk> token)
# 5. Padding and unknown sharing the same index
# Correct: separate special tokens
vocab = build_vocab_from_iterator(
yield_tokens(train_iter),
specials=['<unk>', '<pad>'], # 0 and 1
min_freq=2 # Discard rare words
)Tworzenie predykcji dla nowego tekstu
Aby klasyfikować nowe recenzje podczas wnioskowania, stosujemy ten sam potok wstępnego przetwarzania co podczas trenowania: tokenizujemy tekst, zamieniamy tokeny na indeksy, skracamy sekwencję i dodajemy wymiar partii. Model zwraca logit, a zastosowanie funkcji sigmoid przekształca go w prawdopodobieństwo.
Wynik predykcji powyżej 0,5 wskazuje na pozytywny sentyment. Mogą Państwo dostosować ten próg: obniżenie go do 0,3 zwiększa czułość dla pozytywnych recenzji kosztem precyzji. Podczas wnioskowania należy zawsze zastosować model.eval() i torch.no_grad(), aby wyłączyć dropout i uniknąć niepotrzebnego obliczania gradientów.
def predict_sentiment(model, text, vocab, tokenizer, device, max_len=256):
model.eval()
tokens = vocab(tokenizer(text))[:max_len]
tensor = torch.tensor(tokens, dtype=torch.long).unsqueeze(0).to(device)
with torch.no_grad():
logit = model(tensor)
prob = torch.sigmoid(logit).item()
return 'positive' if prob > 0.5 else 'negative', prob
label, score = predict_sentiment(
model,
'This film was absolutely brilliant. Loved every scene!',
vocab, tokenizer, device
)
print(f'Sentiment: {label} ({score:.3f})')LSTM a BERT w analizie sentymentu
Chociaż sieci LSTM były najnowocześniejszym rozwiązaniem w dziedzinie NLP do około 2018 roku, modele oparte na transformerach, takie jak BERT, dominują obecnie w większości testów porównawczych NLP. BERT osiąga około 93–95% na zbiorze IMDB, w porównaniu z około 87–90% dla LSTM.
Sieci LSTM pozostają jednak istotne z kilku powodów: są znacznie wydajniejsze obliczeniowo dla bardzo długich sekwencji (transformatory skalują się kwadratowo wraz z długością sekwencji), dobrze sprawdzają się w przypadku danych szeregów czasowych, gdzie sekwencyjny charakter danych jest nieodłączny, oraz zajmują znacznie mniej pamięci. Zrozumienie sieci LSTM zapewnia również podstawy pojęciowe potrzebne do zrozumienia, jak mechanizmy uwagi stanowią ich udoskonalenie.
Szybkie sprawdzenie
Sprawdź swoją wiedzę na temat klasyfikacji sekwencji opartej na LSTM z tego урокu.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo, że: architektura sequence-to-one odczytuje całą sekwencję wejściową i mapuje ją na pojedynczą etykietę, wykorzystując końcowy stan ukryty LSTM; potok wstępnego przetwarzania tekstu wymaga tokenizacji, zbudowania słownika i dopełnienia sekwencji przed przekazaniem danych do modelu; a warstwy embeddingów przekształcają dyskretne indeksy słów w gęste, uczone wektory, które odwzorowują znaczenie semantyczne. Następnie omówimy transfer learning, w którym wstępnie wytrenowane wagi modelu zastępują trenowanie od podstaw.
Ucz się Python dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 30
- Lekcje
- 120
Często zadawane pytania
Czy lekcja „Sequence-to-One: analiza sentymentu za pomocą LSTM” jest bezpłatna?
Tak — pełny tekst „Sequence-to-One: analiza sentymentu za pomocą LSTM” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Machine Learning Academy, przejdź na CoddyKit PRO. Kurs Machine Learning Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Sequence-to-One: analiza sentymentu za pomocą LSTM”?
Uczą się Państwo tworzyć kompleksowy klasyfikator sentymentu: tokenizować recenzje, osadzać słowa za pomocą nn.Embedding, przetwarzać je przez LSTM i klasyfikować końcowy stan ukryty. Ćwiczysz Machine Learning Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Machine Learning Academy?
Nie wymagamy żadnego doświadczenia. Machine Learning Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Sequence-to-One: analiza sentymentu za pomocą LSTM”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Machine Learning Academy?
Tak. Każda lekcja Machine Learning Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Klasyczne sieci RNN: stan ukryty i rozwijanie sekwencji
- Problem zanikającego gradientu w głębokich krokach czasowych
- Komórka LSTM: bramki wejścia, zapominania i wyjścia
- Sequence-to-One: analiza sentymentu za pomocą LSTM