.. _sec_lstm: Bộ nhớ ngắn hạn dài (LSTM) ========================== Thách thức giải quyết việc bảo tồn thông tin dài hạn và bỏ qua đầu vào ngắn hạn trong các mô hình biến tiềm ẩn đã tồn tại trong một thời gian dài. Một trong những cách tiếp cận sớm nhất để giải quyết vấn đề này là bộ nhớ ngắn hạn dài (LSTM) :cite:`Hochreiter.Schmidhuber.1997`. Nó chia sẻ nhiều tài sản của GRU. Điều thú vị là LSTMs có thiết kế phức tạp hơn một chút so với Grus nhưng trước Grus gần hai thập kỷ. Gated Memory Cell ----------------- Được cho là thiết kế của LSTM được lấy cảm hứng từ cổng logic của máy tính. LSTM giới thiệu một tế bào *memory cell* (hoặc *cell* nói ngắn) có hình dạng giống như trạng thái ẩn (một số văn học coi ô nhớ là một loại đặc biệt của trạng thái ẩn), được thiết kế để ghi lại thông tin bổ sung. Để kiểm soát tế bào bộ nhớ, chúng ta cần một số cổng. Một cổng là cần thiết để đọc các mục từ ô. Chúng tôi sẽ đề cập đến điều này là *cổng đầu ra*. Một cổng thứ hai là cần thiết để quyết định khi nào nên đọc dữ liệu vào ô. Chúng tôi gọi đây là cổng đầu vào \*\*. Cuối cùng, chúng ta cần một cơ chế để thiết lập lại nội dung của ô, được chi phối bởi một cổng \* quên \*. Động lực cho một thiết kế như vậy giống như của Grus, cụ thể là có thể quyết định khi nào cần nhớ và khi nào bỏ qua các đầu vào ở trạng thái ẩn thông qua một cơ chế chuyên dụng. Hãy để chúng tôi xem làm thế nào điều này hoạt động trong thực tế. Cổng đầu vào, Cổng quên và Cổng đầu ra ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Cũng giống như trong Grus, việc cấp dữ liệu vào cổng LSTM là đầu vào ở bước thời gian hiện tại và trạng thái ẩn của bước thời gian trước đó, như minh họa trong :numref:`lstm_0`. Chúng được xử lý bởi ba lớp được kết nối hoàn toàn với chức năng kích hoạt sigmoid để tính toán các giá trị của đầu vào, quê. và cổng đầu ra. Kết quả là, các giá trị của ba cổng nằm trong khoảng :math:`(0, 1)`. .. _lstm_0: .. figure:: ../img/lstm-0.svg Computing the input gate, the forget gate, and the output gate in an LSTM model. Về mặt toán học, giả sử rằng có :math:`h` đơn vị ẩn, kích thước lô là :math:`n` và số lượng đầu vào là :math:`d`. Do đó, đầu vào là :math:`\mathbf{X}_t \in \mathbb{R}^{n \times d}` và trạng thái ẩn của bước thời gian trước là :math:`\mathbf{H}_{t-1} \in \mathbb{R}^{n \times h}`. Tương ứng, các cổng tại bước thời điểm :math:`t` được định nghĩa như sau: cổng đầu vào là :math:`\mathbf{I}_t \in \mathbb{R}^{n \times h}`, cổng quên là :math:`\mathbf{F}_t \in \mathbb{R}^{n \times h}` và cổng đầu ra là :math:`\mathbf{O}_t \in \mathbb{R}^{n \times h}`. Chúng được tính như sau: .. math:: \begin{aligned} \mathbf{I}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xi} + \mathbf{H}_{t-1} \mathbf{W}_{hi} + \mathbf{b}_i),\\ \mathbf{F}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xf} + \mathbf{H}_{t-1} \mathbf{W}_{hf} + \mathbf{b}_f),\\ \mathbf{O}_t &= \sigma(\mathbf{X}_t \mathbf{W}_{xo} + \mathbf{H}_{t-1} \mathbf{W}_{ho} + \mathbf{b}_o), \end{aligned} trong đó :math:`\mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h}` và :math:`\mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h}` là các thông số trọng lượng và :math:`\mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h}` là các thông số thiên vị. Tế bào bộ nhớ ứng cử viên ~~~~~~~~~~~~~~~~~~~~~~~~~ Tiếp theo chúng tôi thiết kế các tế bào bộ nhớ. Vì chúng tôi chưa chỉ định hành động của các cổng khác nhau, trước tiên chúng tôi giới thiệu tế bào bộ nhớ \*ứng cử viên :math:`\tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h}`. Tính toán của nó tương tự như của ba cổng được mô tả ở trên, nhưng sử dụng hàm :math:`\tanh` với phạm vi giá trị cho :math:`(-1, 1)` làm hàm kích hoạt. Điều này dẫn đến phương trình sau tại bước thời điểm :math:`t`: .. math:: \tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c), trong đó :math:`\mathbf{W}_{xc} \in \mathbb{R}^{d \times h}` và :math:`\mathbf{W}_{hc} \in \mathbb{R}^{h \times h}` là các thông số trọng lượng và :math:`\mathbf{b}_c \in \mathbb{R}^{1 \times h}` là một tham số thiên vị. Một minh họa nhanh chóng của tế bào bộ nhớ ứng cử viên được thể hiện trong :numref:`lstm_1`. .. _lstm_1: .. figure:: ../img/lstm-1.svg Computing the candidate memory cell in an LSTM model. Bộ nhớ Cell ~~~~~~~~~~~ Trong Grus, chúng ta có một cơ chế để chi phối đầu vào và quên (hoặc bỏ qua). Tương tự, trong LSTMs, chúng tôi có hai cổng chuyên dụng cho các mục đích như vậy: cổng đầu vào :math:`\mathbf{I}_t` chi phối số lượng chúng tôi tính đến dữ liệu mới thông qua :math:`\tilde{\mathbf{C}}_t` và cổng quên :math:`\mathbf{F}_t` giải quyết bao nhiêu nội dung tế bào bộ nhớ cũ :math:`\mathbf{C}_{t-1} \in \mathbb{R}^{n \times h}` chúng tôi giữ lại. Sử dụng cùng một thủ thuật nhân theo chiều ngang như trước đây, chúng tôi đến phương trình cập nhật sau: .. math:: \mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t. Nếu cổng quên luôn xấp xỉ 1 và cổng đầu vào luôn xấp xỉ 0, các ô bộ nhớ quá khứ :math:`\mathbf{C}_{t-1}` sẽ được lưu theo thời gian và được chuyển sang bước thời gian hiện tại. Thiết kế này được giới thiệu để giảm bớt vấn đề gradient biến mất và để nắm bắt tốt hơn các phụ thuộc tầm xa trong chuỗi. Do đó, chúng tôi đến sơ đồ dòng chảy trong :numref:`lstm_2`. .. _lstm_2: .. figure:: ../img/lstm-2.svg Computing the memory cell in an LSTM model. Nhà nước ẩn ~~~~~~~~~~~ Cuối cùng, chúng ta cần xác định cách tính trạng thái ẩn :math:`\mathbf{H}_t \in \mathbb{R}^{n \times h}`. Đây là nơi cổng đầu ra phát huy tác dụng. Trong LSTM, nó chỉ đơn giản là một phiên bản cổng của :math:`\tanh` của tế bào bộ nhớ. Điều này đảm bảo rằng các giá trị của :math:`\mathbf{H}_t` luôn nằm trong khoảng :math:`(-1, 1)`. .. math:: \mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t). Bất cứ khi nào cổng đầu ra xấp xỉ 1, chúng tôi có hiệu quả truyền tất cả thông tin bộ nhớ đến bộ dự đoán, trong khi đối với cổng đầu ra gần 0, chúng tôi chỉ giữ lại tất cả thông tin trong ô nhớ và không thực hiện xử lý thêm. :numref:`lstm_3` có một minh họa đồ họa của luồng dữ liệu. .. _lstm_3: .. figure:: ../img/lstm-3.svg Computing the hidden state in an LSTM model. Thực hiện từ đầu ---------------- Bây giờ chúng ta hãy thực hiện một LSTM từ đầu. Giống như các thí nghiệm trong :numref:`sec_rnn_scratch`, lần đầu tiên chúng ta tải tập dữ liệu máy thời gian. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python from mxnet import np, npx from mxnet.gluon import rnn from d2l import mxnet as d2l npx.set_np() batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) .. raw:: html
.. raw:: html
.. code:: python import torch from torch import nn from d2l import torch as d2l batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) .. raw:: html
.. raw:: html
.. code:: python import tensorflow as tf from d2l import tensorflow as d2l batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps) .. parsed-literal:: :class: output Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt... .. raw:: html
.. raw:: html
Initializing Model Parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tiếp theo chúng ta cần xác định và khởi tạo các tham số mô hình. Như trước đây, siêu tham số ``num_hiddens`` xác định số đơn vị ẩn. Chúng tôi khởi tạo các trọng lượng sau một phân phối Gaussian với 0,01 độ lệch chuẩn, và chúng tôi đặt các thành kiến là 0. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python def get_lstm_params(vocab_size, num_hiddens, device): num_inputs = num_outputs = vocab_size def normal(shape): return np.random.normal(scale=0.01, size=shape, ctx=device) def three(): return (normal((num_inputs, num_hiddens)), normal((num_hiddens, num_hiddens)), np.zeros(num_hiddens, ctx=device)) W_xi, W_hi, b_i = three() # Input gate parameters W_xf, W_hf, b_f = three() # Forget gate parameters W_xo, W_ho, b_o = three() # Output gate parameters W_xc, W_hc, b_c = three() # Candidate memory cell parameters # Output layer parameters W_hq = normal((num_hiddens, num_outputs)) b_q = np.zeros(num_outputs, ctx=device) # Attach gradients params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] for param in params: param.attach_grad() return params .. raw:: html
.. raw:: html
.. code:: python def get_lstm_params(vocab_size, num_hiddens, device): num_inputs = num_outputs = vocab_size def normal(shape): return torch.randn(size=shape, device=device)*0.01 def three(): return (normal((num_inputs, num_hiddens)), normal((num_hiddens, num_hiddens)), torch.zeros(num_hiddens, device=device)) W_xi, W_hi, b_i = three() # Input gate parameters W_xf, W_hf, b_f = three() # Forget gate parameters W_xo, W_ho, b_o = three() # Output gate parameters W_xc, W_hc, b_c = three() # Candidate memory cell parameters # Output layer parameters W_hq = normal((num_hiddens, num_outputs)) b_q = torch.zeros(num_outputs, device=device) # Attach gradients params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] for param in params: param.requires_grad_(True) return params .. raw:: html
.. raw:: html
.. code:: python def get_lstm_params(vocab_size, num_hiddens): num_inputs = num_outputs = vocab_size def normal(shape): return tf.Variable(tf.random.normal(shape=shape, stddev=0.01, mean=0, dtype=tf.float32)) def three(): return (normal((num_inputs, num_hiddens)), normal((num_hiddens, num_hiddens)), tf.Variable(tf.zeros(num_hiddens), dtype=tf.float32)) W_xi, W_hi, b_i = three() # Input gate parameters W_xf, W_hf, b_f = three() # Forget gate parameters W_xo, W_ho, b_o = three() # Output gate parameters W_xc, W_hc, b_c = three() # Candidate memory cell parameters # Output layer parameters W_hq = normal((num_hiddens, num_outputs)) b_q = tf.Variable(tf.zeros(num_outputs), dtype=tf.float32) # Attach gradients params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] return params .. raw:: html
.. raw:: html
Xác định mô hình ~~~~~~~~~~~~~~~~ Trong the initialization function, trạng thái ẩn của LSTM cần trả về một ô bộ nhớ *additional* với giá trị 0 và hình dạng (kích thước lô, số đơn vị ẩn). Do đó chúng tôi nhận được sự khởi tạo trạng thái sau đây. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python def init_lstm_state(batch_size, num_hiddens, device): return (np.zeros((batch_size, num_hiddens), ctx=device), np.zeros((batch_size, num_hiddens), ctx=device)) .. raw:: html
.. raw:: html
.. code:: python def init_lstm_state(batch_size, num_hiddens, device): return (torch.zeros((batch_size, num_hiddens), device=device), torch.zeros((batch_size, num_hiddens), device=device)) .. raw:: html
.. raw:: html
.. code:: python def init_lstm_state(batch_size, num_hiddens): return (tf.zeros(shape=(batch_size, num_hiddens)), tf.zeros(shape=(batch_size, num_hiddens))) .. raw:: html
.. raw:: html
mô hình thực tế được định nghĩa giống như những gì chúng ta đã thảo luận trước đây: cung cấp ba cổng và một tế bào bộ nhớ phụ trợ. Lưu ý rằng chỉ có trạng thái ẩn được chuyển đến lớp đầu ra. Các tế bào bộ nhớ :math:`\mathbf{C}_t` không trực tiếp tham gia vào việc tính toán đầu ra. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python def lstm(inputs, state, params): [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params (H, C) = state outputs = [] for X in inputs: I = npx.sigmoid(np.dot(X, W_xi) + np.dot(H, W_hi) + b_i) F = npx.sigmoid(np.dot(X, W_xf) + np.dot(H, W_hf) + b_f) O = npx.sigmoid(np.dot(X, W_xo) + np.dot(H, W_ho) + b_o) C_tilda = np.tanh(np.dot(X, W_xc) + np.dot(H, W_hc) + b_c) C = F * C + I * C_tilda H = O * np.tanh(C) Y = np.dot(H, W_hq) + b_q outputs.append(Y) return np.concatenate(outputs, axis=0), (H, C) .. raw:: html
.. raw:: html
.. code:: python def lstm(inputs, state, params): [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q] = params (H, C) = state outputs = [] for X in inputs: I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i) F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f) O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o) C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c) C = F * C + I * C_tilda H = O * torch.tanh(C) Y = (H @ W_hq) + b_q outputs.append(Y) return torch.cat(outputs, dim=0), (H, C) .. raw:: html
.. raw:: html
.. code:: python def lstm(inputs, state, params): W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c, W_hq, b_q = params (H, C) = state outputs = [] for X in inputs: X=tf.reshape(X,[-1,W_xi.shape[0]]) I = tf.sigmoid(tf.matmul(X, W_xi) + tf.matmul(H, W_hi) + b_i) F = tf.sigmoid(tf.matmul(X, W_xf) + tf.matmul(H, W_hf) + b_f) O = tf.sigmoid(tf.matmul(X, W_xo) + tf.matmul(H, W_ho) + b_o) C_tilda = tf.tanh(tf.matmul(X, W_xc) + tf.matmul(H, W_hc) + b_c) C = F * C + I * C_tilda H = O * tf.tanh(C) Y = tf.matmul(H, W_hq) + b_q outputs.append(Y) return tf.concat(outputs, axis=0), (H,C) .. raw:: html
.. raw:: html
Đào tạo và Dự đoán ~~~~~~~~~~~~~~~~~~ Chúng ta hãy đào tạo một LSTM giống như những gì chúng tôi đã làm trong :numref:`sec_gru`, bằng cách khởi tạo lớp ``RNNModelScratch`` như được giới thiệu trong :numref:`sec_rnn_scratch`. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu() num_epochs, lr = 500, 1 model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params, init_lstm_state, lstm) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) .. parsed-literal:: :class: output perplexity 1.2, 9030.1 tokens/sec on gpu(0) time travellerit s against reason said filbywh w beewhith of man travellery off the bredarsoncacimitabdeat fil so walk thisk .. figure:: output_lstm_86eb9f_51_1.svg .. raw:: html
.. raw:: html
.. code:: python vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu() num_epochs, lr = 500, 1 model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params, init_lstm_state, lstm) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) .. parsed-literal:: :class: output perplexity 1.1, 21930.5 tokens/sec on cuda:0 time travelleryou can show black is white by argument said filby traveller a forescereopeer three dimensions and the latter .. figure:: output_lstm_86eb9f_54_1.svg .. raw:: html
.. raw:: html
.. code:: python vocab_size, num_hiddens, device_name = len(vocab), 256, d2l.try_gpu()._device_name num_epochs, lr = 500, 1 strategy = tf.distribute.OneDeviceStrategy(device_name) with strategy.scope(): model = d2l.RNNModelScratch(len(vocab), num_hiddens, init_lstm_state, lstm, get_lstm_params) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) .. parsed-literal:: :class: output perplexity 1.1, 3704.1 tokens/sec on /GPU:0 time traveller for so it will be convenient to speak of himwas e travellerification of this know a feetsmin sfiflly thus thi .. figure:: output_lstm_86eb9f_57_1.svg .. raw:: html
.. raw:: html
Thiết tập --------- Sử dụng API cấp cao, chúng ta có thể khởi tạo trực tiếp một mô hình ``LSTM``. Điều này đóng gói tất cả các chi tiết cấu hình mà chúng tôi đã thực hiện rõ ràng ở trên. Mã này nhanh hơn đáng kể vì nó sử dụng các toán tử được biên dịch hơn là Python cho nhiều chi tiết mà chúng tôi đã nêu chi tiết trước đây. .. raw:: html
mxnetpytorchtensorflow
.. raw:: html
.. code:: python lstm_layer = rnn.LSTM(num_hiddens) model = d2l.RNNModel(lstm_layer, len(vocab)) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) .. parsed-literal:: :class: output perplexity 1.1, 158023.4 tokens/sec on gpu(0) time traveller but now you begin to seethe object of my investig traveller he way be able to stop oraccelerate his drift alo .. figure:: output_lstm_86eb9f_63_1.svg .. raw:: html
.. raw:: html
.. code:: python num_inputs = vocab_size lstm_layer = nn.LSTM(num_inputs, num_hiddens) model = d2l.RNNModel(lstm_layer, len(vocab)) model = model.to(device) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device) .. parsed-literal:: :class: output perplexity 1.0, 311960.7 tokens/sec on cuda:0 time traveller with a slight accession ofcheerfulness really thi travelleryou can show black is white by argument said filby .. figure:: output_lstm_86eb9f_66_1.svg .. raw:: html
.. raw:: html
.. code:: python lstm_cell = tf.keras.layers.LSTMCell(num_hiddens, kernel_initializer='glorot_uniform') lstm_layer = tf.keras.layers.RNN(lstm_cell, time_major=True, return_sequences=True, return_state=True) device_name = d2l.try_gpu()._device_name strategy = tf.distribute.OneDeviceStrategy(device_name) with strategy.scope(): model = d2l.RNNModel(lstm_layer, vocab_size=len(vocab)) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, strategy) .. parsed-literal:: :class: output perplexity 1.0, 8551.9 tokens/sec on /GPU:0 time traveller for so it will be convenient to speak of himwas e traveller with a slight accession ofcheerfulness really thi .. figure:: output_lstm_86eb9f_69_1.svg .. raw:: html
.. raw:: html
LSTMs là mô hình autoregressive biến tiềm ẩn nguyên mẫu với điều khiển trạng thái không tầm thường. Nhiều biến thể của chúng đã được đề xuất trong những năm qua, ví dụ, nhiều lớp, kết nối còn lại, các loại chính quy hóa khác nhau. Tuy nhiên, việc đào tạo LSTMs và các mô hình trình tự khác (chẳng hạn như Grus) khá tốn kém do sự phụ thuộc tầm xa của trình tự. Sau đó chúng ta sẽ gặp phải các mô hình thay thế như máy biến áp có thể được sử dụng trong một số trường hợp. Tóm tắt ------- - LSTMs có ba loại cổng: cổng đầu vào, cổng quên và cổng đầu ra kiểm soát luồng thông tin. - Đầu ra lớp ẩn của LSTM bao gồm trạng thái ẩn và ô nhớ. Chỉ có trạng thái ẩn được truyền vào lớp đầu ra. Tế bào bộ nhớ hoàn toàn bên trong. - LSTMs có thể làm giảm bớt độ dốc biến mất và bùng nổ. Bài tập ------- 1. Điều chỉnh các siêu tham số và phân tích ảnh hưởng của chúng đối với thời gian chạy, bối rối và trình tự đầu ra. 2. Làm thế nào bạn sẽ cần phải thay đổi mô hình để tạo ra các từ thích hợp như trái ngược với chuỗi các ký tự? 3. So sánh chi phí tính toán cho Grus, LSTMs và RNN thông thường cho một chiều ẩn nhất định. Đặc biệt chú ý đến chi phí đào tạo và suy luận. 4. Vì ô bộ nhớ ứng cử viên đảm bảo rằng phạm vi giá trị nằm trong khoảng :math:`-1` và :math:`1` bằng cách sử dụng hàm :math:`\tanh`, tại sao trạng thái ẩn cần sử dụng lại chức năng :math:`\tanh` để đảm bảo rằng phạm vi giá trị đầu ra nằm trong khoảng từ :math:`-1` và :math:`1`? 5. Triển khai một mô hình LSTM cho dự đoán chuỗi thời gian hơn là dự đoán chuỗi ký tự. .. raw:: html
mxnetpytorch
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html
`Discussions `__ .. raw:: html
.. raw:: html