.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_model_construction:
Tầng và Khối
============
.. raw:: html
Khi lần đầu giới thiệu về các mạng nơ-ron, ta tập trung vào các mô hình
tuyến tính với một đầu ra duy nhất. Như vậy toàn bộ mô hình chỉ chứa một
nơ-ron. Lưu ý rằng một nơ-ron đơn lẻ (i) nhận một vài đầu vào; (ii) tạo
một đầu ra (*vô hướng*) tương ứng; và (iii) có một tập các tham số liên
quan có thể được cập nhật để tối ưu một hàm mục tiêu nào đó mà ta quan
tâm. Sau đó, khi bắt đầu nghĩ về các mạng có nhiều đầu ra, ta tận dụng
các phép tính vector để mô tả nguyên một *tầng* nơ-ron. Cũng giống như
các nơ-ron riêng lẻ, các tầng (i) nhận một số đầu vào, (ii) tạo các đầu
ra tương ứng, và (iii) được mô tả bằng một tập các tham số có thể điều
chỉnh được. Trong hồi quy softmax, bản thân *tầng* duy nhất ấy chính là
một *mô hình*. Thậm chí đối với các perceptron đa tầng, ta vẫn có thể
nghĩ về chúng theo cấu trúc cơ bản này.
.. raw:: html
Điều thú vị là đối với các perceptron đa tầng, cả *mô hình* và các *tầng
cấu thành* đều chia sẻ cấu trúc này. (Toàn bộ) mô hình nhận các đầu vào
thô (các đặc trưng), tạo các đầu ra (các dự đoán) và sở hữu các tham số
(được tập hợp từ tất cả các tầng cấu thành). Tương tự, mỗi tầng riêng lẻ
cũng nhận các đầu vào (được cung cấp bởi tầng trước đó), tính toán các
đầu ra (cũng chính là các đầu vào cho tầng tiếp theo), và có một tập các
tham số có thể điều chỉnh thông qua việc cập nhật dựa trên tín hiệu được
truyền ngược từ tầng kế tiếp.
.. raw:: html
Dù bạn có thể nghĩ rằng các nơ-ron, các tầng và các mô hình đã cung cấp
đủ sự trừu tượng để bắt tay vào làm việc, hóa ra sẽ là thuận tiện hơn
khi ta bàn về các thành phần lớn hơn một tầng riêng lẻ nhưng lại nhỏ hơn
toàn bộ mô hình. Ví dụ, kiến trúc ResNet-152, rất phổ biến trong thị
giác máy tính, sở hữu hàng trăm tầng. Nó bao gồm các khuôn mẫu *nhóm
tầng* được lặp lại nhiều lần. Việc lập trình từng tầng của một mạng như
vậy có thể trở nên tẻ nhạt. Mối quan tâm này không chỉ là trên lý thuyết
— các khuôn mẫu thiết kế như vậy rất phổ biến trong thực tế. Kiến trúc
ResNet được đề cập ở trên đã giành chiến thắng trong hai cuộc thi thị
giác máy tính ImageNet và COCO năm 2015, trong cả bài toán nhận dạng và
bài toán phát hiện :cite:`He.Zhang.Ren.ea.2016` và vẫn là một kiến
trúc được tin dùng cho nhiều bài toán thị giác. Các kiến trúc tương tự,
trong đó các tầng được sắp xếp thành những khuôn mẫu lặp lại, hiện đã
trở nên thông dụng ở nhiều lĩnh vực khác, bao gồm cả xử lý ngôn ngữ tự
nhiên và xử lý tiếng nói.
.. raw:: html
.. raw:: html
.. raw:: html
Để lập trình các mạng phức tạp này, ta sẽ giới thiệu khái niệm *khối*
trong mạng nơ-ron. Một khối có thể mô tả một tầng duy nhất, một mảng đa
tầng hoặc toàn bộ một mô hình! Dưới góc nhìn xây dựng phần mềm, một
``Block`` (Khối) là một *lớp*. Bất kỳ một lớp con nào của ``Block`` đều
phải định nghĩa phương thức ``forward`` để chuyển hóa đầu vào thành đầu
ra và phải lưu trữ mọi tham số cần thiết. Lưu ý rằng có một vài
``Block`` sẽ không yêu cầu chứa bất kỳ tham số nào cả! Ngoài ra, một
``Block`` phải sở hữu một phương thức ``backward`` cho mục đích tính
toán gradient. May mắn thay, nhờ sự trợ giúp đắc lực của gói
``autograd`` (được giới thiệu trong :numref:`chap_preliminaries`) nên
khi định nghĩa ``Block``, ta chỉ cần quan tâm đến các tham số và hàm
``forward``.
.. raw:: html
Một lợi ích khi làm việc ở mức độ trừu tượng ``Block`` đó là ta có thể
kết hợp chúng, thường là theo phương pháp đệ quy, để tạo ra các thành
phần lớn hơn (xem hình minh họa trong :numref:`fig_blocks`).
.. raw:: html
.. _fig_blocks:
.. figure:: ../img/blocks.svg
Nhiều tầng được kết hợp để tạo thành các khối
.. raw:: html
Bằng cách định nghĩa các khối với độ phức tạp tùy ý, các mạng nơ-ron
phức tạp có thể được lập trình với mã nguồn ngắn gọn một cách đáng ngạc
nhiên.
.. raw:: html
Để bắt đầu, ta sẽ xem lại các khối mà ta đã sử dụng để lập trình
perceptron đa tầng (:numref:`sec_mlp_gluon`). Đoạn mã nguồn sau tạo ra
một mạng gồm một tầng ẩn kết nối đầy đủ với 256 nút sử dụng hàm kích
hoạt ReLU, theo sau là một *tầng đầu ra* kết nối đầy đủ với 10 nút
(không có hàm kích hoạt).
.. code:: python
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()
x = np.random.uniform(size=(2, 20))
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
net(x)
.. parsed-literal::
:class: output
array([[ 0.06240272, -0.03268593, 0.02582653, 0.02254182, -0.03728798,
-0.04253786, 0.00540613, -0.01364186, -0.09915452, -0.02272738],
[ 0.02816677, -0.03341204, 0.03565666, 0.02506382, -0.04136416,
-0.04941845, 0.01738528, 0.01081961, -0.09932579, -0.01176298]])
.. raw:: html
Trong ví dụ này, ta đã xây dựng mô hình bằng cách khởi tạo một đối tượng
``nn.Sequential`` và gán vào biến ``net``. Sau đó, ta gọi phương thức
``add`` nhiều lần để nối các tầng theo thứ tự mà chúng sẽ được thực thi.
Nói một cách ngắn gọn, ``nn.Sequential`` định nghĩa một loại ``Block``
đặc biệt có nhiệm vụ duy trì một danh sách chứa các ``Block`` cấu thành
được sắp xếp theo thứ tự nhất định. Phương thức ``add`` chỉ đơn giản hỗ
trợ việc thêm liên tiếp từng ``Block`` vào trong danh sách đó. Lưu ý
rằng mỗi tầng là một thực thể của lớp ``Dense``, và bản thân lớp
``Dense`` lại là một lớp con của ``Block``. Hàm ``forward`` cũng rất đơn
giản: nó xâu chuỗi từng ``Block`` trong danh sách lại với nhau, chuyển
đầu ra của từng khối thành đầu vào cho khối tiếp theo. Lưu ý rằng cho
đến giờ, ta đã gọi mô hình thông qua ``net(X)`` để thu được đầu ra. Thực
ra đây chỉ là một cách viết tắt của ``net.forward(X)``, một thủ thuật
Python khéo léo đạt được thông qua hàm ``__call__`` của lớp ``Block``.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Một Khối Tùy chỉnh
------------------
.. raw:: html
Có lẽ cách dễ nhất để hiểu rõ hơn ``nn.Block`` hoạt động như thế nào là
tự lập trình nó. Trước khi tự lập trình một ``Block`` tùy chỉnh, hãy
cùng tóm tắt ngắn gọn các chức năng cơ bản mà một ``Block`` phải cung
cấp:
.. raw:: html
1. Phương thức ``forward`` nhận đối số là dữ liệu đầu vào.
2. Phương thức ``forward`` trả về một giá trị đầu ra. Lưu ý rằng đầu ra
có thể có kích thước khác với đầu vào. Ví dụ, tầng Dense đầu tiên
trong mô hình phía trên nhận đầu vào có kích thước tùy ý nhưng trả về
đầu ra có kích thước 256.
3. Tính gradient của đầu ra theo đầu vào bằng phương thức ``backward``,
thường thì việc này được thực hiện tự động.
4. Lưu trữ và cung cấp quyền truy cập tới các tham số cần thiết để tiến
hành phương thức tính toán ``forward``.
5. Khởi tạo các tham số này khi cần thiết.
.. raw:: html
Trong đoạn mã dưới đây, chúng ta lập trình từ đầu một Block (Khối) tương
đương với một perceptron đa tầng chỉ có một tầng ẩn và 256 nút ẩn, cùng
một tầng đầu ra 10 chiều. Lưu ý rằng lớp ``MLP`` bên dưới đây kế thừa từ
lớp ``Block``. Ta sẽ phụ thuộc nhiều vào các phương thức của lớp cha, và
chỉ tự viết phương thức ``__init__`` và ``forward``.
.. code:: python
from mxnet.gluon import nn
class MLP(nn.Block):
# Declare a layer with model parameters. Here, we declare two fully
# connected layers
def __init__(self, **kwargs):
# Call the constructor of the MLP parent class Block to perform the
# necessary initialization. In this way, other function parameters can
# also be specified when constructing an instance, such as the model
# parameter, params, described in the following sections
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Dense(256, activation='relu') # Hidden layer
self.output = nn.Dense(10) # Output layer
# Define the forward computation of the model, that is, how to return the
# required model output based on the input x
def forward(self, x):
return self.output(self.hidden(x))
.. raw:: html
Để bắt đầu, ta sẽ tập trung vào phương thức ``forward``. Lưu ý rằng nó
nhận giá trị đầu vào ``x``, tính toán tầng biểu diễn ẩn
(``self.hidden(x)``) và trả về các giá trị logit
(``self.output( ... )``). Ở cách lập trình MLP này, cả hai tầng trên đều
là biến thực thể (*instance variables*). Để thấy tại sao điều này có lý,
tưởng tượng ta khởi tạo hai MLP, ``net1`` và ``net2``, và huấn luyện
chúng với dữ liệu khác nhau. Dĩ nhiên là ta mong đợi chúng đại diện cho
hai mô hình học khác nhau.
.. raw:: html
Ta khởi tạo các tầng của MLP trong phương thức ``__init__`` (hàm khởi
tạo) và sau đó gọi các tầng này mỗi khi ta gọi phương thức ``forward``.
Hãy chú ý một vài chi tiết quan trọng. Đầu tiên, phương thức
``__init__`` tùy chỉnh của ta gọi phương thức ``__init__`` của lớp cha
thông qua ``super(MLP, self).__init__(**kwargs)`` để tránh việc viết lại
cùng một phần mã nguồn áp dụng cho hầu hết các khối. Chúng ta sau đó
khởi tạo hai tầng ``Dense``, gán chúng lần lượt là ``self.hidden`` và
``self.output``. Chú ý rằng trừ khi đang phát triển một toán tử mới,
chúng ta không cần lo lắng về lan truyền ngược (phương thức
``backward``) hoặc khởi tạo tham số (phương thức ``initialize``). Gluon
sẽ tự động khởi tạo các phương thức đó. Hãy cùng thử nghiệm điều này:
.. code:: python
net = MLP()
net.initialize()
net(x)
.. parsed-literal::
:class: output
array([[-0.03989594, -0.1041471 , 0.06799038, 0.05245074, 0.02526059,
-0.00640342, 0.04182098, -0.01665319, -0.02067346, -0.07863817],
[-0.03612847, -0.07210436, 0.09159479, 0.07890771, 0.02494172,
-0.01028665, 0.01732428, -0.02843242, 0.03772651, -0.06671704]])
.. raw:: html
Một ưu điểm chính của phép trừu tượng hóa ``Block`` là tính linh hoạt
của nó. Ta có thể kế thừa từ lớp ``Block`` để tạo các tầng (chẳng hạn
như lớp ``Dense`` được cung cấp bởi Gluon), toàn bộ cả mô hình (như
``MLP`` ở phía trên) hoặc các thành phần đa dạng với độ phức tạp vừa
phải. Ta sẽ tận dụng tính linh hoạt này xuyên suốt ở các chương sau, đặc
biệt khi làm việc với các mạng nơ-ron tích chập.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Khối Tuần tự
------------
.. raw:: html
Bây giờ ta có thể có cái nhìn rõ hơn về cách mà lớp ``Sequential`` (Tuần
tự) hoạt động. Nhắc lại rằng ``Sequential`` được thiết kế để xâu chuỗi
các Khối lại với nhau. Để tự xây dựng một lớp ``MySequential`` đơn giản,
ta chỉ cần định nghĩa hai phương thức chính sau: 1. Phương thức ``add``
nhằm đẩy từng Block một vào trong danh sách. 2. Phương thức ``forward``
nhằm truyền một đầu vào qua chuỗi các Blocks (theo thứ tự mà chúng được
nối).
.. raw:: html
Lớp ``MySequential`` dưới đây cung cấp tính năng giống như lớp
``Sequential`` mặc định của Gluon:
.. code:: python
class MySequential(nn.Block):
def add(self, block):
# Here, block is an instance of a Block subclass, and we assume it has
# a unique name. We save it in the member variable _children of the
# Block class, and its type is OrderedDict. When the MySequential
# instance calls the initialize function, the system automatically
# initializes all members of _children
self._children[block.name] = block
def forward(self, x):
# OrderedDict guarantees that members will be traversed in the order
# they were added
for block in self._children.values():
x = block(x)
return x
.. raw:: html
Phương thức ``add`` thêm một Block đơn vào từ điển có thứ tự
``_children``. Bạn có thể thắc mắc tại sao mỗi ``Block`` của Gluon sở
hữu một thuộc tính ``_children`` và tại sao ta sử dụng nó thay vì tự tạo
một danh sách Python. Thật ra, ưu điểm chính của ``_children`` là trong
quá trình khởi tạo trọng số ban đầu của các khối, Gluon sẽ tự động tìm
các khối con có trọng số cần được khởi tạo trong từ điển này.
.. raw:: html
Khi phương thức ``forward`` của khối ``MySequential`` được gọi, các
``Block`` sẽ được thực thi theo thứ tự mà chúng được thêm vào. Bây giờ
ta có thể lập trình lại một MLP sử dụng lớp ``MySequential``.
.. code:: python
net = MySequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
net(x)
.. parsed-literal::
:class: output
array([[-0.07645682, -0.01130233, 0.04952145, -0.04651389, -0.04131573,
-0.05884133, -0.0621381 , 0.01311472, -0.01379425, -0.02514282],
[-0.05124625, 0.00711231, -0.00155935, -0.07555379, -0.06675334,
-0.01762914, 0.00589084, 0.01447191, -0.04330775, 0.03317726]])
.. raw:: html
Chú ý rằng việc sử dụng ``MySequential`` giống hệt với đoạn mã mà ta đã
viết trước đó cho lớp ``Sequential`` của Gluon (được mô tả trong
:numref:`sec_mlp_gluon`).
.. raw:: html
.. raw:: html
.. raw:: html
Thực thi Mã trong Phương thức ``forward``
-----------------------------------------
.. raw:: html
Lớp ``nn.Sequential`` giúp việc xây dựng mô hình trở nên dễ hơn, cho
phép ta xây dựng các kiến trúc mới mà không cần phải tự định nghĩa một
lớp riêng. Tuy nhiên, không phải tất cả mô hình đều có cấu trúc chuỗi
xích đơn giản. Khi cần phải linh hoạt hơn, ta vẫn sẽ muốn định nghĩa
từng ``Block`` theo cách của mình, ví dụ như khi muốn sử dụng luồng điều
khiển Python trong lượt truyền xuôi. Hơn nữa, ta cũng có thể muốn thực
hiện các phép toán tùy ý thay vì chỉ dựa vào các tầng mạng nơ-ron được
định nghĩa từ trước.
.. raw:: html
Độc giả có thể nhận ra rằng tất cả phép toán trong mạng cho tới giờ đều
thao tác trên các giá trị kích hoạt và tham số của mạng. Tuy nhiên,
trong một vài trường hợp, ta có thể muốn kết hợp thêm các hằng số. Chúng
không phải là kết quả của tầng trước mà cũng không phải là tham số có
thể cập nhật được. Trong Gluon, ta gọi chúng là tham số *không đổi*
(*constant parameter*). Ví dụ ta muốn một tầng tính hàm
:math:`f(\mathbf{x},\mathbf{w}) = c \cdot \mathbf{w}^\top \mathbf{x}`,
trong đó :math:`\mathbf{x}`, :math:`\mathbf{w}` là tham số, và :math:`c`
là một hằng số cho trước được giữ nguyên giá trị trong suốt quá trình
tối ưu hóa.
.. raw:: html
Khai báo các hằng số một cách tường minh (bằng ``get_constant``) giúp
Gluon tăng tốc độ thực thi. Trong đoạn mã sau, ta lập trình một mô hình
mà không hề dễ lắp ráp nếu sử dụng ``Sequential`` và các tầng được định
nghĩa trước.
.. code:: python
class FixedHiddenMLP(nn.Block):
def __init__(self, **kwargs):
super(FixedHiddenMLP, self).__init__(**kwargs)
# Random weight parameters created with the get_constant are not
# iterated during training (i.e., constant parameters)
self.rand_weight = self.params.get_constant(
'rand_weight', np.random.uniform(size=(20, 20)))
self.dense = nn.Dense(20, activation='relu')
def forward(self, x):
x = self.dense(x)
# Use the constant parameters created, as well as the relu
# and dot functions
x = npx.relu(np.dot(x, self.rand_weight.data()) + 1)
# Reuse the fully connected layer. This is equivalent to sharing
# parameters with two fully connected layers
x = self.dense(x)
# Here in Control flow, we need to call asscalar to return the scalar
# for comparison
while np.abs(x).sum() > 1:
x /= 2
return x.sum()
.. raw:: html
Trong mô hình ``FixedHiddenMLP``, ta lập trình một tầng ẩn có trọng số
(``self.rand_ weight``) được khởi tạo ngẫu nhiên và giữ nguyên giá trị
về sau. Trọng số này không phải là một tham số mô hình, vì vậy nó không
được cập nhật khi sử dụng lan truyền ngược. Sau đó, đầu ra của tầng *cố
định* này được đưa vào tầng ``Dense``.
.. raw:: html
Lưu ý rằng trước khi trả về giá trị đầu ra, mô hình của ta đã làm điều
gì đó bất thường. Ta đã chạy một vòng lặp ``while``, lấy vector đầu ra
chia cho :math:`2` cho đến khi nó thỏa mãn điều kiện
``np.abs(x).sum() > 1``. Cuối cùng, ta gán giá trị đầu ra bằng tổng các
phần tử trong ``x``. Theo sự hiểu biết của chúng tôi, không có mạng
nơ-ron tiêu chuẩn nào thực hiện phép toán này. Lưu ý rằng phép toán đặc
biệt này có thể không hữu ích gì trong các công việc ngoài thực tế. Mục
đích của chúng tôi ở đây là chỉ cho độc giả thấy được cách tích hợp một
đoạn mã tùy ý vào luồng tính toán của mạng nơ-ron.
.. code:: python
net = FixedHiddenMLP()
net.initialize()
net(x)
.. parsed-literal::
:class: output
array(0.52637565)
.. raw:: html
Với Gluon, ta có thể kết hợp nhiều cách khác nhau để lắp ráp các
``Block`` lại. Trong ví dụ dưới đây, ta lồng các ``Block`` với nhau theo
nhiều cách sáng tạo.
.. code:: python
class NestMLP(nn.Block):
def __init__(self, **kwargs):
super(NestMLP, self).__init__(**kwargs)
self.net = nn.Sequential()
self.net.add(nn.Dense(64, activation='relu'),
nn.Dense(32, activation='relu'))
self.dense = nn.Dense(16, activation='relu')
def forward(self, x):
return self.dense(self.net(x))
chimera = nn.Sequential()
chimera.add(NestMLP(), nn.Dense(20), FixedHiddenMLP())
chimera.initialize()
chimera(x)
.. parsed-literal::
:class: output
array(0.97720534)
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Biên dịch Mã nguồn
------------------
.. raw:: html
| Những người đọc có tâm có thể sẽ bắt đầu lo lắng về hiệu năng của một
vài đoạn mã trên. Sau cùng thì, chúng ta có rất nhiều thao tác truy
cập từ điển, thực thi mã lập trình và rất nhiều thứ “đậm chất Python”
khác xuất hiện trong thứ mà lẽ ra nên là một thư viện học sâu hiệu
năng cao. Vấn đề của `Khóa Trình thông dịch Toàn cục (Global
Interpreter
Lock) `__ trong
Python khá phổ biến. Trong bối cảnh học sâu, ta lo sợ rằng GPU cực kỳ
nhanh của ta có thể sẽ phải đợi CPU “rùa bò” chạy xong những dòng lệnh
Python trước khi nó có thể nhận tác vụ chạy tiếp theo. Cách tốt nhất
để tăng tốc Python là tránh không sử dụng nó. Gluon làm việc này bằng
cách cho phép việc Hybrid hóa (:numref:`sec_hybridize`). Ở đây,
trình thông dịch của Python sẽ thực thi một Khối trong lần chạy đầu
tiên.
| Môi trường chạy của Gluon sẽ ghi lại những gì đang diễn ra và trong
lần chạy tiếp theo, nó sẽ thực hiện các tác vụ gọi trong Python một
cách vắn tắt hơn. Điều này có thể giúp tăng tốc độ chạy đáng kể trong
một vài trường hợp, tuy nhiên, ta cần quan tâm tới việc luồng điều
khiển (như trên) sẽ dẫn đến những nhánh khác nhau với mỗi lần truyền
qua mạng. Chúng tôi khuyến khích những độc giả có hứng thú sau khi
hoàn tất chương này hãy đọc thêm mục hybrid hóa
(:numref:`sec_hybridize`) để tìm hiểu về quá trình biên dịch.
.. raw:: html
Tóm tắt
-------
.. raw:: html
- Các tầng trong mạng nơ-ron là các Khối.
- Nhiều tầng có thể cấu thành một Khối.
- Nhiều Khối có thể cấu thành một Khối.
- Một Khối có thể chứa các đoạn mã nguồn.
- Các Khối đảm nhiệm nhiều tác vụ bao gồm khởi tạo tham số và lan
truyền ngược.
- Việc gắn kết các tầng và khối một cách tuần tự được đảm nhiệm bởi
Khối ``Sequential``.
.. raw:: html
Bài tập
-------
.. raw:: html
1. Vấn đề gì sẽ xảy ra nếu ta bỏ hàm ``asscalar`` trong lớp
``FixedHiddenMLP``?
2. Vấn đề gì sẽ xảy ra nếu\ ``self.net`` được định nghĩa trong thực thể
``Sequential`` ở lớp ``NestMLP`` được đổi thành
``self.net = [nn.Dense(64, activation='relu'), nn. Dense(32, activation='relu')]``?
3. Hãy lập trình một khối nhận đối số là hai khối khác, ví dụ như
``net1`` và ``net2``, và trả về kết quả là phép nối các giá trị đầu
ra của cả hai mạng đó khi thực hiện lượt truyền xuôi.
4. Giả sử bạn muốn nối nhiều thực thể của cùng một mạng với nhau. Hãy
lập trình một hàm để tạo ra nhiều thực thể của cùng một mạng và dùng
chúng để tạo thành một mạng lớn hơn (các hàm này trong thiết kế phần
mềm được gọi là Factory Function).
.. raw:: html
.. raw:: html
Thảo luận
---------
- `Tiếng Anh `__
- `Tiếng Việt `__
Những người thực hiện
---------------------
Bản dịch trong trang này được thực hiện bởi:
- Đoàn Võ Duy Thanh
- Nguyễn Duy Du
- Phạm Minh Đức
- Nguyễn Lê Quang Nhật
- Lê Khắc Hồng Phúc
- Trần Yến Thy
- Phạm Hồng Vinh
- Lý Phi Long