{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Mạng nơ-ron hồi quy\n", "\n", "Trong module trước, chúng ta đã sử dụng các biểu diễn ngữ nghĩa phong phú của văn bản và một bộ phân loại tuyến tính đơn giản trên các embeddings. Kiến trúc này giúp nắm bắt ý nghĩa tổng hợp của các từ trong một câu, nhưng nó không tính đến **thứ tự** của các từ, vì thao tác tổng hợp trên các embeddings đã loại bỏ thông tin này từ văn bản gốc. Do các mô hình này không thể mô hình hóa thứ tự từ, chúng không thể giải quyết các nhiệm vụ phức tạp hoặc mơ hồ hơn như tạo văn bản hoặc trả lời câu hỏi.\n", "\n", "Để nắm bắt ý nghĩa của chuỗi văn bản, chúng ta cần sử dụng một kiến trúc mạng nơ-ron khác, được gọi là **mạng nơ-ron hồi quy**, hay RNN. Trong RNN, chúng ta đưa câu qua mạng từng ký hiệu một lần, và mạng tạo ra một **trạng thái**, sau đó chúng ta đưa trạng thái này trở lại mạng cùng với ký hiệu tiếp theo.\n", "\n", "Với chuỗi đầu vào các token $X_0,\\dots,X_n$, RNN tạo ra một chuỗi các khối mạng nơ-ron và huấn luyện chuỗi này từ đầu đến cuối bằng cách sử dụng lan truyền ngược. Mỗi khối mạng nhận một cặp $(X_i,S_i)$ làm đầu vào và tạo ra $S_{i+1}$ làm kết quả. Trạng thái cuối cùng $S_n$ hoặc đầu ra $X_n$ được đưa vào một bộ phân loại tuyến tính để tạo ra kết quả. Tất cả các khối mạng đều chia sẻ cùng một trọng số và được huấn luyện từ đầu đến cuối chỉ với một lần lan truyền ngược.\n", "\n", "Vì các vector trạng thái $S_0,\\dots,S_n$ được truyền qua mạng, nó có khả năng học các phụ thuộc tuần tự giữa các từ. Ví dụ, khi từ *not* xuất hiện ở đâu đó trong chuỗi, mạng có thể học cách phủ định một số phần tử trong vector trạng thái, dẫn đến phủ định.\n", "\n", "> Vì các trọng số của tất cả các khối RNN trong hình đều được chia sẻ, hình ảnh này có thể được biểu diễn dưới dạng một khối duy nhất (bên phải) với một vòng lặp hồi quy, nơi trạng thái đầu ra của mạng được truyền lại vào đầu vào.\n", "\n", "Hãy cùng xem cách mạng nơ-ron hồi quy có thể giúp chúng ta phân loại tập dữ liệu tin tức.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n", "Building vocab...\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "from torchnlp import *\n", "train_dataset, test_dataset, classes, vocab = load_dataset()\n", "vocab_size = len(vocab)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bộ phân loại RNN đơn giản\n", "\n", "Trong trường hợp RNN đơn giản, mỗi đơn vị hồi tiếp là một mạng tuyến tính đơn giản, nhận một vector đầu vào được nối và một vector trạng thái, sau đó tạo ra một vector trạng thái mới. PyTorch biểu diễn đơn vị này bằng lớp `RNNCell`, và một mạng lưới các đơn vị như vậy - bằng lớp `RNN`.\n", "\n", "Để định nghĩa một bộ phân loại RNN, trước tiên chúng ta sẽ áp dụng một lớp nhúng để giảm chiều của từ vựng đầu vào, sau đó sử dụng lớp RNN ở phía trên:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class RNNClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,h = self.rnn(x)\n", " return self.fc(x.mean(dim=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Note:** Chúng ta sử dụng lớp nhúng chưa được huấn luyện ở đây để đơn giản hóa, nhưng để đạt kết quả tốt hơn, chúng ta có thể sử dụng lớp nhúng đã được huấn luyện trước với Word2Vec hoặc GloVe embeddings, như đã được mô tả trong bài học trước. Để hiểu rõ hơn, bạn có thể điều chỉnh mã này để hoạt động với các nhúng đã được huấn luyện trước.\n", "\n", "Trong trường hợp của chúng ta, chúng ta sẽ sử dụng bộ tải dữ liệu được đệm, vì vậy mỗi batch sẽ có một số chuỗi được đệm với cùng độ dài. Lớp RNN sẽ nhận chuỗi các tensor nhúng và tạo ra hai đầu ra:\n", "* $x$ là chuỗi các đầu ra của cell RNN tại mỗi bước\n", "* $h$ là trạng thái ẩn cuối cùng cho phần tử cuối cùng của chuỗi\n", "\n", "Sau đó, chúng ta áp dụng một bộ phân loại tuyến tính kết nối đầy đủ để lấy số lượng lớp.\n", "\n", "> **Note:** RNN khá khó huấn luyện, bởi vì khi các cell RNN được mở rộng theo chiều dài chuỗi, số lượng lớp liên quan đến quá trình lan truyền ngược sẽ rất lớn. Do đó, chúng ta cần chọn tốc độ học nhỏ và huấn luyện mạng trên tập dữ liệu lớn hơn để đạt kết quả tốt. Quá trình này có thể mất khá nhiều thời gian, vì vậy việc sử dụng GPU được ưu tiên.\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.3090625\n", "6400: acc=0.38921875\n", "9600: acc=0.4590625\n", "12800: acc=0.511953125\n", "16000: acc=0.5506875\n", "19200: acc=0.57921875\n", "22400: acc=0.6070089285714285\n", "25600: acc=0.6304296875\n", "28800: acc=0.6484027777777778\n", "32000: acc=0.66509375\n", "35200: acc=0.6790056818181818\n", "38400: acc=0.6929166666666666\n", "41600: acc=0.7035817307692308\n", "44800: acc=0.7137276785714286\n", "48000: acc=0.72225\n", "51200: acc=0.73001953125\n", "54400: acc=0.7372794117647059\n", "57600: acc=0.7436631944444444\n", "60800: acc=0.7503947368421052\n", "64000: acc=0.75634375\n", "67200: acc=0.7615773809523809\n", "70400: acc=0.7662642045454545\n", "73600: acc=0.7708423913043478\n", "76800: acc=0.7751822916666666\n", "80000: acc=0.7790625\n", "83200: acc=0.7825\n", "86400: acc=0.7858564814814815\n", "89600: acc=0.7890513392857142\n", "92800: acc=0.7920474137931034\n", "96000: acc=0.7952708333333334\n", "99200: acc=0.7982258064516129\n", "102400: acc=0.80099609375\n", "105600: acc=0.8037594696969697\n", "108800: acc=0.8060569852941176\n" ] } ], "source": [ "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n", "net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bộ nhớ ngắn dài (LSTM)\n", "\n", "Một trong những vấn đề chính của RNN cổ điển là vấn đề **độ dốc biến mất**. Vì RNN được huấn luyện từ đầu đến cuối trong một lần truyền ngược, nên rất khó để truyền lỗi đến các lớp đầu tiên của mạng, dẫn đến mạng không thể học được mối quan hệ giữa các token xa nhau. Một trong những cách để tránh vấn đề này là giới thiệu **quản lý trạng thái rõ ràng** bằng cách sử dụng các **cổng**. Có hai kiến trúc nổi tiếng nhất thuộc loại này: **Bộ nhớ ngắn dài** (LSTM) và **Đơn vị chuyển tiếp có cổng** (GRU).\n", "\n", "![Hình ảnh minh họa một tế bào bộ nhớ ngắn dài](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "Mạng LSTM được tổ chức theo cách tương tự như RNN, nhưng có hai trạng thái được truyền từ lớp này sang lớp khác: trạng thái thực tế $c$, và vector ẩn $h$. Tại mỗi đơn vị, vector ẩn $h_i$ được nối với đầu vào $x_i$, và chúng kiểm soát những gì xảy ra với trạng thái $c$ thông qua các **cổng**. Mỗi cổng là một mạng nơ-ron với hàm kích hoạt sigmoid (đầu ra trong phạm vi $[0,1]$), có thể được coi như một mặt nạ bit khi nhân với vector trạng thái. Có các cổng sau đây (từ trái sang phải trên hình ảnh ở trên):\n", "* **Cổng quên** lấy vector ẩn và xác định những thành phần nào của vector $c$ cần quên, và những thành phần nào cần giữ lại.\n", "* **Cổng đầu vào** lấy một số thông tin từ đầu vào và vector ẩn, rồi chèn nó vào trạng thái.\n", "* **Cổng đầu ra** biến đổi trạng thái thông qua một lớp tuyến tính với kích hoạt $\\tanh$, sau đó chọn một số thành phần của nó bằng cách sử dụng vector ẩn $h_i$ để tạo ra trạng thái mới $c_{i+1}$.\n", "\n", "Các thành phần của trạng thái $c$ có thể được coi như các cờ hiệu có thể bật hoặc tắt. Ví dụ, khi chúng ta gặp một cái tên *Alice* trong chuỗi, chúng ta có thể giả định rằng nó đề cập đến một nhân vật nữ, và bật cờ trong trạng thái rằng chúng ta có danh từ nữ trong câu. Khi chúng ta gặp thêm cụm từ *và Tom*, chúng ta sẽ bật cờ rằng chúng ta có danh từ số nhiều. Do đó, bằng cách thao tác trạng thái, chúng ta có thể theo dõi các thuộc tính ngữ pháp của các phần trong câu.\n", "\n", "> **Note**: Một tài liệu tuyệt vời để hiểu rõ nội bộ của LSTM là bài viết [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) của Christopher Olah.\n", "\n", "Mặc dù cấu trúc nội bộ của tế bào LSTM có thể trông phức tạp, PyTorch đã ẩn việc triển khai này bên trong lớp `LSTMCell`, và cung cấp đối tượng `LSTM` để đại diện cho toàn bộ lớp LSTM. Do đó, việc triển khai bộ phân loại LSTM sẽ khá giống với RNN đơn giản mà chúng ta đã thấy ở trên:\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class LSTMClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,(h,c) = self.rnn(x)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.259375\n", "6400: acc=0.25859375\n", "9600: acc=0.26177083333333334\n", "12800: acc=0.2784375\n", "16000: acc=0.313\n", "19200: acc=0.3528645833333333\n", "22400: acc=0.3965625\n", "25600: acc=0.4385546875\n", "28800: acc=0.4752777777777778\n", "32000: acc=0.505375\n", "35200: acc=0.5326704545454546\n", "38400: acc=0.5557552083333334\n", "41600: acc=0.5760817307692307\n", "44800: acc=0.5954910714285714\n", "48000: acc=0.6118333333333333\n", "51200: acc=0.62681640625\n", "54400: acc=0.6404779411764706\n", "57600: acc=0.6520138888888889\n", "60800: acc=0.662828947368421\n", "64000: acc=0.673546875\n", "67200: acc=0.6831547619047619\n", "70400: acc=0.6917897727272727\n", "73600: acc=0.6997146739130434\n", "76800: acc=0.707109375\n", "80000: acc=0.714075\n", "83200: acc=0.7209134615384616\n", "86400: acc=0.727037037037037\n", "89600: acc=0.7326674107142858\n", "92800: acc=0.7379633620689655\n", "96000: acc=0.7433645833333333\n", "99200: acc=0.7479032258064516\n", "102400: acc=0.752119140625\n", "105600: acc=0.7562405303030303\n", "108800: acc=0.76015625\n", "112000: acc=0.7641339285714286\n", "115200: acc=0.7677777777777778\n", "118400: acc=0.7711233108108108\n" ] }, { "data": { "text/plain": [ "(0.03487814127604167, 0.7728)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Chuỗi được đóng gói\n", "\n", "Trong ví dụ của chúng ta, chúng ta đã phải thêm các vector số 0 để làm đầy tất cả các chuỗi trong minibatch. Mặc dù điều này dẫn đến việc lãng phí bộ nhớ, nhưng với RNN, vấn đề quan trọng hơn là các tế bào RNN bổ sung được tạo ra cho các phần tử đầu vào đã được làm đầy, những phần tử này tham gia vào quá trình huấn luyện nhưng không mang thông tin đầu vào quan trọng. Sẽ tốt hơn nhiều nếu chỉ huấn luyện RNN với kích thước chuỗi thực tế.\n", "\n", "Để làm điều đó, một định dạng đặc biệt để lưu trữ chuỗi đã làm đầy được giới thiệu trong PyTorch. Giả sử chúng ta có một minibatch đầu vào đã làm đầy trông như thế này:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "Ở đây, số 0 đại diện cho các giá trị làm đầy, và vector độ dài thực tế của các chuỗi đầu vào là `[5,3,1]`.\n", "\n", "Để huấn luyện RNN hiệu quả với chuỗi đã làm đầy, chúng ta muốn bắt đầu huấn luyện nhóm đầu tiên của các tế bào RNN với minibatch lớn (`[1,6,9]`), sau đó kết thúc xử lý chuỗi thứ ba, và tiếp tục huấn luyện với các minibatch nhỏ hơn (`[2,7]`, `[3,8]`), và cứ thế. Vì vậy, chuỗi được đóng gói được biểu diễn dưới dạng một vector - trong trường hợp của chúng ta là `[1,6,9,2,7,3,8,4,5]`, và vector độ dài (`[5,3,1]`), từ đó chúng ta có thể dễ dàng tái tạo lại minibatch đã làm đầy ban đầu.\n", "\n", "Để tạo chuỗi được đóng gói, chúng ta có thể sử dụng hàm `torch.nn.utils.rnn.pack_padded_sequence`. Tất cả các lớp hồi quy, bao gồm RNN, LSTM và GRU, đều hỗ trợ chuỗi được đóng gói làm đầu vào, và tạo ra đầu ra được đóng gói, có thể được giải mã bằng cách sử dụng `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "Để có thể tạo chuỗi được đóng gói, chúng ta cần truyền vector độ dài vào mạng, và do đó chúng ta cần một hàm khác để chuẩn bị các minibatch:\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def pad_length(b):\n", " # build vectorized sequence\n", " v = [encode(x[1]) for x in b]\n", " # compute max length of a sequence in this minibatch and length sequence itself\n", " len_seq = list(map(len,v))\n", " l = max(len_seq)\n", " return ( # tuple of three tensors - labels, padded features, length sequence\n", " torch.LongTensor([t[0]-1 for t in b]),\n", " torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n", " torch.tensor(len_seq)\n", " )\n", "\n", "train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mạng thực tế sẽ rất giống với `LSTMClassifier` ở trên, nhưng bước `forward` sẽ nhận cả minibatch đã được đệm và vector độ dài của các chuỗi. Sau khi tính embedding, chúng ta tính packed sequence, truyền nó qua lớp LSTM, và sau đó giải nén kết quả trở lại.\n", "\n", "> **Lưu ý**: Thực tế, chúng ta không sử dụng kết quả đã được giải nén `x`, vì chúng ta sử dụng đầu ra từ các lớp ẩn trong các phép tính tiếp theo. Do đó, chúng ta có thể loại bỏ hoàn toàn bước giải nén khỏi đoạn mã này. Lý do chúng tôi đặt nó ở đây là để bạn có thể dễ dàng chỉnh sửa đoạn mã này, trong trường hợp bạn cần sử dụng đầu ra của mạng trong các phép tính tiếp theo.\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class LSTMPackClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x, lengths):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n", " pad_x,(h,c) = self.rnn(pad_x)\n", " x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 8, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.285625\n", "6400: acc=0.33359375\n", "9600: acc=0.3876041666666667\n", "12800: acc=0.44078125\n", "16000: acc=0.4825\n", "19200: acc=0.5235416666666667\n", "22400: acc=0.5559821428571429\n", "25600: acc=0.58609375\n", "28800: acc=0.6116666666666667\n", "32000: acc=0.63340625\n", "35200: acc=0.6525284090909091\n", "38400: acc=0.668515625\n", "41600: acc=0.6822596153846154\n", "44800: acc=0.6948214285714286\n", "48000: acc=0.7052708333333333\n", "51200: acc=0.71521484375\n", "54400: acc=0.7239889705882353\n", "57600: acc=0.7315277777777778\n", "60800: acc=0.7388486842105263\n", "64000: acc=0.74571875\n", "67200: acc=0.7518303571428572\n", "70400: acc=0.7576988636363636\n", "73600: acc=0.7628940217391305\n", "76800: acc=0.7681510416666667\n", "80000: acc=0.7728125\n", "83200: acc=0.7772235576923077\n", "86400: acc=0.7815393518518519\n", "89600: acc=0.7857700892857142\n", "92800: acc=0.7895043103448276\n", "96000: acc=0.7930520833333333\n", "99200: acc=0.7959072580645161\n", "102400: acc=0.798994140625\n", "105600: acc=0.802064393939394\n", "108800: acc=0.8051378676470589\n", "112000: acc=0.8077857142857143\n", "115200: acc=0.8104600694444445\n", "118400: acc=0.8128293918918919\n" ] }, { "data": { "text/plain": [ "(0.029785829671223958, 0.8138166666666666)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Lưu ý:** Bạn có thể đã nhận thấy tham số `use_pack_sequence` mà chúng ta truyền vào hàm huấn luyện. Hiện tại, hàm `pack_padded_sequence` yêu cầu tensor độ dài chuỗi phải nằm trên thiết bị CPU, và do đó hàm huấn luyện cần tránh di chuyển dữ liệu độ dài chuỗi sang GPU khi huấn luyện. Bạn có thể xem xét việc triển khai hàm `train_emb` trong tệp [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN hai chiều và nhiều lớp\n", "\n", "Trong các ví dụ của chúng ta, tất cả các mạng hồi quy đều hoạt động theo một hướng, từ đầu chuỗi đến cuối chuỗi. Điều này có vẻ tự nhiên, vì nó giống cách chúng ta đọc và nghe lời nói. Tuy nhiên, trong nhiều trường hợp thực tế, chúng ta có thể truy cập ngẫu nhiên vào chuỗi đầu vào, nên việc thực hiện tính toán hồi quy theo cả hai hướng có thể hợp lý. Các mạng như vậy được gọi là **RNN hai chiều**, và chúng có thể được tạo bằng cách truyền tham số `bidirectional=True` vào hàm khởi tạo của RNN/LSTM/GRU.\n", "\n", "Khi làm việc với mạng hai chiều, chúng ta sẽ cần hai vector trạng thái ẩn, một cho mỗi hướng. PyTorch mã hóa các vector này thành một vector có kích thước gấp đôi, điều này khá tiện lợi, vì thông thường bạn sẽ truyền trạng thái ẩn kết quả vào lớp tuyến tính kết nối đầy đủ, và bạn chỉ cần tính đến sự gia tăng kích thước này khi tạo lớp.\n", "\n", "Mạng hồi quy, dù một chiều hay hai chiều, đều nắm bắt các mẫu nhất định trong một chuỗi và có thể lưu trữ chúng vào vector trạng thái hoặc truyền vào đầu ra. Tương tự như mạng tích chập, chúng ta có thể xây dựng một lớp hồi quy khác trên lớp đầu tiên để nắm bắt các mẫu cấp cao hơn, được xây dựng từ các mẫu cấp thấp do lớp đầu tiên trích xuất. Điều này dẫn đến khái niệm **RNN nhiều lớp**, bao gồm hai hoặc nhiều mạng hồi quy, trong đó đầu ra của lớp trước được truyền vào lớp tiếp theo làm đầu vào.\n", "\n", "![Hình ảnh minh họa một mạng RNN LSTM nhiều lớp](../../../../../translated_images/vi/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*Hình ảnh từ [bài viết tuyệt vời này](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) của Fernando López*\n", "\n", "PyTorch giúp việc xây dựng các mạng như vậy trở nên dễ dàng, vì bạn chỉ cần truyền tham số `num_layers` vào hàm khởi tạo của RNN/LSTM/GRU để tự động tạo nhiều lớp hồi quy. Điều này cũng có nghĩa là kích thước của vector trạng thái ẩn sẽ tăng lên tương ứng, và bạn cần tính đến điều này khi xử lý đầu ra của các lớp hồi quy.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNNs cho các nhiệm vụ khác\n", "\n", "Trong bài học này, chúng ta đã thấy rằng RNNs có thể được sử dụng để phân loại chuỗi, nhưng thực tế, chúng có thể xử lý nhiều nhiệm vụ khác như tạo văn bản, dịch máy, và nhiều hơn nữa. Chúng ta sẽ xem xét những nhiệm vụ đó trong bài học tiếp theo.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Tuyên bố miễn trừ trách nhiệm**: \nTài liệu này đã được dịch bằng dịch vụ dịch thuật AI [Co-op Translator](https://github.com/Azure/co-op-translator). Mặc dù chúng tôi cố gắng đảm bảo độ chính xác, xin lưu ý rằng các bản dịch tự động có thể chứa lỗi hoặc không chính xác. Tài liệu gốc bằng ngôn ngữ bản địa nên được coi là nguồn thông tin chính thức. Đối với các thông tin quan trọng, khuyến nghị sử dụng dịch vụ dịch thuật chuyên nghiệp bởi con người. Chúng tôi không chịu trách nhiệm cho bất kỳ sự hiểu lầm hoặc diễn giải sai nào phát sinh từ việc sử dụng bản dịch này.\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "coopTranslator": { "original_hash": "522ee52ae3d5ae933e283286254e9a55", "translation_date": "2025-08-29T16:16:59+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "vi" } }, "nbformat": 4, "nbformat_minor": 2 }