17.1. Mạng Đối sinh

Xuyên suốt phần lớn cuốn sách này, ta đã nói về việc làm thế nào để thực hiện những dự đoán. Ở dưới dạng nào đi nữa, ta đã cho mạng nơ-ron sâu học cách ánh xạ từ các mẫu dữ liệu sang các nhãn. Kiểu học này được gọi là học phân biệt, ví dụ như phân biệt ảnh chó và mèo. Phân loại và hồi quy là hai ví dụ của việc học phân biệt. Mạng nơ-ron được huấn luyện bằng phương pháp lan truyền ngược đã đảo lộn mọi thứ ta từng biết về học phân biệt trên các tập dữ liệu lớn phức tạp. Độ chính xác của tác vụ phân loại ảnh có độ phân giải cao đã đạt tới mức độ như người (với một số điều kiện) từ chỗ không thể sử dụng được chỉ trong 5-6 năm gần đây. May mắn cho bạn, sẽ không có một bài diễn thuyết nữa về các tác vụ phân biệt khác mà ở đó mạng nơ-ron sâu thực hiện tốt một cách đáng kinh ngạc.

Nhưng học máy còn làm được nhiều hơn là chỉ giải quyết các tác vụ phân biệt. Chẳng hạn, với một tập dữ liệu không nhãn cho trước, ta có thể xây dựng một mô hình nắm bắt chính xác các đặc tính của tập dữ liệu này. Với một mô hình như vậy, ta có thể tổng hợp ra các mẫu dữ liệu mới giống như phân phối của dữ liệu dùng để huấn luyện. Ví dụ, với một kho lớn dữ liệu ảnh khuôn mặt cho trước, ta có thể tạo ra một ảnh như thật, giống như nó được lấy từ cùng tập dữ liệu. Kiểu học này được gọi là mô hình hóa tác vụ sinh (generative modelling).

Cho đến gần đây, ta không có phương pháp nào để có thể tổng hợp các ảnh mới như thật. Nhưng thành công của mạng nơ-ron sâu với học phân biệt đã mở ra những khả năng mới. Một xu hướng lớn trong hơn ba năm vừa qua là việc áp dụng mạng sâu phân biệt để vượt qua các thách thức trong các bài toán mà nhìn chung không được xem là học có giám sát. Các mô hình ngôn ngữ mạng nơ-ron hồi tiếp là một ví dụ về việc sử dụng một mạng phân biệt (được huấn luyện để dự đoán ký tự kế tiếp) mà một khi được huấn luyện có thể vận hành như một mô hình sinh.

Trong năm 2014, có một bài báo mang tính đột phá đã giới thiệu Mạng đối sinh (Generative Adversarial Network - GAN) [Goodfellow et al., 2014], một phương pháp khôn khéo tận dụng sức mạnh của các mô hình phân biệt để có được các mô hình sinh tốt. Về cốt lõi, GAN dựa trên ý tưởng là một bộ sinh dữ liệu là tốt nếu ta không thể chỉ ra đâu là dữ liệu giả và đâu là dữ liệu thật. Trong thống kê, điều này được gọi là bài kiểm tra từ hai tập mẫu - một bài kiểm tra để trả lời câu hỏi liệu tập dữ liệu X={x1,,xn}X={x1,,xn} có được rút ra từ cùng một phân phối. Sự khác biệt chính giữa hầu hết những bài nghiên cứu thống kê và GAN là GAN sử dụng ý tưởng này theo kiểu có tính cách xây dựng. Nói cách khác, thay vì chỉ huấn luyện một mô hình để nói “này, hai tập dữ liệu này có vẻ như không đến từ cùng một phân phối”, thì chúng sử dụng phương pháp kiểm tra trên hai tập mẫu để cung cấp tín hiệu cho việc huấn luyện cho một mô hình sinh. Điều này cho phép ta cải thiện bộ sinh dữ liệu tới khi nó sinh ra thứ gì đó giống như dữ liệu thực. Ở mức tối thiểu nhất, nó cần lừa được bộ phân loại, kể cả nếu bộ phân loại của ta là một mạng nơ-ron sâu tân tiến nhất.

../_images/gan.svg

Fig. 17.1.1 Mạng Đối Sinh

Kiến trúc của mạng đối sinh được miêu tả trong hình Fig. 17.1.1. Như ta có thể thấy, có hai thành phần trong kiến trúc của GAN - đầu tiên, ta cần một thiết bị (giả sử, một mạng sâu nhưng nó có thể là bất kỳ thứ gì, chẳng hạn như công cụ kết xuất đồ họa trò chơi) có khả năng tạo ra dữ liệu giống thật. Nếu ta đang làm việc với hình ảnh, mô hình cần tạo ra hình ảnh. Nếu ta đang làm việc với giọng nói, mô hình cần tạo ra được chuỗi âm thanh, v.v. Ta gọi mô hình này là mạng sinh (generator network). Thành phần thứ hai là mạng phân biệt (discriminator network). Nó cố gắng phân biệt dữ liệu giả và thật. Cả hai mạng này sẽ cạnh tranh với nhau. Mạng sinh sẽ cố gắng đánh lừa mạng phân biệt. Đồng thời, mạng phân biệt sẽ thích nghi với dữ liệu giả vừa mới tạo ra. Thông tin thu được sẽ được dùng để cải thiện mạng sinh, và cứ tiếp tục như vậy.

Mạng phân biệt là một bộ phân loại nhị phân nhằm phân biệt xem đầu vào x là thật (từ dữ liệu thật) hoặc giả (từ mạng sinh). Thông thường, đầu ra của mạng phân biệt là một số vô hướng oR dự đoán cho đầu vào x, chằng hạn như sử dụng một tầng kết nối đầy đủ với kích thước ẩn 1 và sau đó sẽ được đưa qua hàm sigmoid để nhận được xác suất dự đoán D(x)=1/(1+eo). Giả sử nhãn y cho dữ liệu thật là 10 cho dữ liệu giả. Ta sẽ huấn luyện mạng phân biệt để cực tiểu hóa mất mát entropy chéo, nghĩa là,

(17.1.1)minD{ylogD(x)(1y)log(1D(x))},

Đối với mạng sinh, trước tiên nó tạo ra một vài tham số ngẫu nhiên zRd từ một nguồn, ví dụ, phân phối chuẩn zN(0,1). Ta thường gọi z như là một biến tiềm ẩn. Mục tiêu của mạng sinh là đánh lừa mạng phân biệt để phân loại x=G(z) là dữ liệu thật, nghĩa là, ta muốn D(G(z))1. Nói cách khác, cho trước một mạng phân biệt D, ta sẽ cập nhật tham số của mạng sinh G nhằm cực đại hóa mất mát entropy chéo khi y=0, tức là,

(17.1.2)maxG{(1y)log(1D(G(z)))}=maxG{log(1D(G(z)))}.

Nếu như mạng sinh làm tốt, thì D(x)1 để mất mát gần 0, kết quả là các gradient sẽ trở nên quá nhỏ để tạo ra được sự tiến bộ đáng kể cho mạng phân biệt. Vì vậy, ta sẽ cực tiểu hóa mất mát như sau:

(17.1.3)minG{ylog(D(G(z)))}=minG{log(D(G(z)))},

trong đó chỉ đưa x=G(z) vào mạng phân biệt nhưng cho trước nhãn y=1.

Nói tóm lại, DG đang chơi trò “minimax” (cực tiểu hóa cực đại) với một hàm mục tiêu toàn diện như sau:

(17.1.4)minDmaxG{ExDatalogD(x)EzNoiselog(1D(G(z)))}.

Rất nhiều ứng dụng của GAN liên quan tới hình ảnh. Để ví dụ, chúng ta sẽ bắt đầu với việc khớp một phân phối đơn giản trước. Ta sẽ minh họa bằng việc cho thấy việc gì sẽ xảy ra nếu sử dụng GAN để tạo một bộ ước lượng kém hiệu quả nhất thế giới cho một phân phối Gauss. Hãy tiến hành nào.

%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
Copy to clipboard

17.1.1. Sinh một vài Dữ liệu “thật”

Vì đây có thể là một ví dụ nhàm chán nhất, ta chỉ đơn giản sinh dữ liệu lấy từ một phân phối Gauss.

X = np.random.normal(0.0, 1, (1000, 2))
A = np.array([[1, 2], [-0.1, 0.5]])
b = np.array([1, 2])
data = np.dot(X, A) + b
Copy to clipboard

Dựa vào đoạn mã trên, dữ liệu này là một phân phối Gauss được dịch chuyển một cách tùy ý với trung bình b và ma trận hiệp phương sai ATA.

d2l.set_figsize()
d2l.plt.scatter(d2l.numpy(data[:100, 0]), d2l.numpy(data[:100, 1]));
print(f'The covariance matrix is\n{np.dot(A.T, A)}')
Copy to clipboard
The covariance matrix is
[[1.01 1.95]
 [1.95 4.25]]
../_images/output_gan_vn_cf970e_5_1.svg
batch_size = 8
data_iter = d2l.load_array((data,), batch_size)
Copy to clipboard

17.1.2. Bộ Sinh

Bộ sinh sẽ là một mạng đơn giản nhất có thể - một mô hình tuyến tính đơn tầng. Đó là vì chúng ta sẽ sử dụng mạng tuyến tính này cùng với bộ sinh dữ liệu từ phân phối Gauss. Vậy nên, nó chỉ cần học những tham số của phân phối này để làm giả dữ liệu một cách hoàn hảo.

net_G = nn.Sequential()
net_G.add(nn.Dense(2))
Copy to clipboard

17.1.3. Bộ Phân biệt

Đối với bộ phân biệt, nó sẽ hơi khác một chút: ta sẽ sử dụng một MLP 3 tầng để khiến mọi thứ trở nên thú vị hơn.

net_D = nn.Sequential()
net_D.add(nn.Dense(5, activation='tanh'),
          nn.Dense(3, activation='tanh'),
          nn.Dense(1))
Copy to clipboard

17.1.4. Huấn luyện

Đầu tiên, ta định nghĩa một hàm để cập nhật bộ phân biệt.

#@save
def update_D(X, Z, net_D, net_G, loss, trainer_D):
    """Update discriminator."""
    batch_size = X.shape[0]
    ones = np.ones((batch_size,), ctx=X.ctx)
    zeros = np.zeros((batch_size,), ctx=X.ctx)
    with autograd.record():
        real_Y = net_D(X)
        fake_X = net_G(Z)
        # Do not need to compute gradient for `net_G`, detach it from
        # computing gradients.
        fake_Y = net_D(fake_X.detach())
        loss_D = (loss(real_Y, ones) + loss(fake_Y, zeros)) / 2
    loss_D.backward()
    trainer_D.step(batch_size)
    return float(loss_D.sum())
Copy to clipboard

Bộ sinh cũng được cập nhật theo cách tương tự. Ở đây, ta sử dụng lại làm mất mát entropy chéo nhưng thay nhãn của dữ liệu giả từ 0 thành 1.

#@save
def update_G(Z, net_D, net_G, loss, trainer_G):
    """Update generator."""
    batch_size = Z.shape[0]
    ones = np.ones((batch_size,), ctx=Z.ctx)
    with autograd.record():
        # We could reuse `fake_X` from `update_D` to save computation
        fake_X = net_G(Z)
        # Recomputing `fake_Y` is needed since `net_D` is changed
        fake_Y = net_D(fake_X)
        loss_G = loss(fake_Y, ones)
    loss_G.backward()
    trainer_G.step(batch_size)
    return float(loss_G.sum())
Copy to clipboard

Cả bộ phân biệt lẫn bộ sinh hoạt động như một bộ hồi quy logistic nhị phân với mất mát entropy chéo. Ta sử dụng Adam để làm mượt quá trình huấn luyện. Với mỗi lần lặp, đầu tiên ta cập nhật bộ phân biệt và sau đó đến bộ sinh. Ta sẽ theo dõi cả giá trị mất mát lẫn những dữ liệu được sinh ra.

def train(net_D, net_G, data_iter, num_epochs, lr_D, lr_G, latent_dim, data):
    loss = gluon.loss.SigmoidBCELoss()
    net_D.initialize(init=init.Normal(0.02), force_reinit=True)
    net_G.initialize(init=init.Normal(0.02), force_reinit=True)
    trainer_D = gluon.Trainer(net_D.collect_params(),
                              'adam', {'learning_rate': lr_D})
    trainer_G = gluon.Trainer(net_G.collect_params(),
                              'adam', {'learning_rate': lr_G})
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)
    for epoch in range(num_epochs):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3)  # loss_D, loss_G, num_examples
        for X in data_iter:
            batch_size = X.shape[0]
            Z = np.random.normal(0, 1, size=(batch_size, latent_dim))
            metric.add(update_D(X, Z, net_D, net_G, loss, trainer_D),
                       update_G(Z, net_D, net_G, loss, trainer_G),
                       batch_size)
        # Visualize generated examples
        Z = np.random.normal(0, 1, size=(100, latent_dim))
        fake_X = net_G(Z).asnumpy()
        animator.axes[1].cla()
        animator.axes[1].scatter(data[:, 0], data[:, 1])
        animator.axes[1].scatter(fake_X[:, 0], fake_X[:, 1])
        animator.axes[1].legend(['real', 'generated'])
        # Show the losses
        loss_D, loss_G = metric[0]/metric[2], metric[1]/metric[2]
        animator.add(epoch + 1, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec')
Copy to clipboard

Bây giờ, ta xác định các siêu tham số để khớp với phân phối Gauss.

lr_D, lr_G, latent_dim, num_epochs = 0.05, 0.005, 2, 20
train(net_D, net_G, data_iter, num_epochs, lr_D, lr_G,
      latent_dim, d2l.numpy(data[:100]))
Copy to clipboard
loss_D 0.694, loss_G 0.692, 1535.9 examples/sec
../_images/output_gan_vn_cf970e_18_1.svg

17.1.5. Tóm tắt

  • Mạng đối sinh (Generative adversarial networks - GAN) được cấu thành bởi hai mạng sâu, bộ sinh và bộ phân biệt.
  • Bộ sinh tạo các ảnh gần với ảnh thật nhất có thể nhằm đánh lừa bộ phân biệt, thông qua tối đa hóa mất mát entropy chéo, nói cách khác, maxlog(D(x)).
  • Bộ phân biệt cố gắng phân biệt những ảnh được tạo với ảnh thật, thông qua tối thiểu hóa mất mát entropy chéo, nói cách khác, minylogD(x)(1y)log(1D(x)).

17.1.6. Bài tập

Liệu có tồn tại điểm cân bằng mà tại đó bộ sinh là người chiến thắng, nói cách khác, bộ phân biệt không thể phân biệt được hai phân phối trên dữ liệu hữu hạn?

17.1.7. Thảo luận

17.1.8. 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 Mai Hoàng Long
  • Lê Khắc Hồng Phúc
  • Phạm Hồng Vinh
  • Lý Phi Long
  • Nguyễn Văn Cường
  • Nguyễn Lê Quang Nhật
  • Phạm Minh Đức
  • Nguyễn Thái Bình

Lần cập nhật gần nhất: 05/10/2020. (Cập nhật lần cuối từ nội dung gốc: 17/09/2020)