In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

In [ ]:
#@title MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

Khám phá Overfitting và Underfitting

Lưu ý: Cộng đồng TensorFlow tại Việt Nam đã và đang dịch những tài liệu này từ nguyên bản tiếng Anh. Những bản dịch này được hoàn thiện dựa trên sự nỗ lực đóng góp từ cộng đồng lập trình viên sử dụng TensorFlow, và có thể không đảm bảo được tính cập nhật của bản dịch đối với Tài liệu chính thức bằng tiếng Anh này. Nếu bạn có bất kỳ đề xuất nào nhằm cải thiện bản dịch này, vui lòng tạo Pull request đến kho chứa trên GitHub của tensorflow/docs-l10n. Để đăng ký dịch hoặc cải thiện nội dung bản dịch, các bạn hãy liên hệ và đặt vấn đề tại docs-vi@tensorflow.org.

Như thường lệ, code trong ví dụ này dùng API tf.keras, các bạn có thể đọc thêm trong TensorFlow Keras guide.

Trong cả hai ví dụ trước - phân loại bình luận phim và dự đoán độ hiệu quả về năng lượng - chúng ta thấy rằng độ chính xác của các mô hình trong việc thẩm định dữ liệu sẽ đạt đỉnh sau khi huấn luyện qua một số giai đoạn, sau đó sẽ bắt đầu giảm.

Nói theo cách khác, các mô hình sẽ trở thành overfitting với dữ liệu huấn luyện. Tìm cách để xử lý overfitting rất quan trọng. Mặc dù việc đạt được độ chính xác cao cho tập huấn luyện là có thể, điều mà chúng ta thực sự mong muốn là phát triển những mô hình mà tổng quát hoá tốt thành một tập kiểm thử (hoặc dữ liệu mà những mô hình chưa thấy bao giờ).

Đối lập với quá khớp là underfitting. Underfitting xảy ra khi vẫn còn có thể cải thiện dữ liệu kiểm tra. Một số lý do hiện tượng này xảy ra: Nếu mô hình chưa đủ mạnh, hoặc bị chính quy hoá quá mức, hoặc đơn giản là chưa được huấn luyện đủ lâu. Điều này có nghĩa là mạng neuron chưa học đủ những xu huớng liên quan trong tập huấn luyện.

Nếu bạn huấn luyện quá lâu, các mô hình sẽ bắt đầu gặp hiện tượng overfitting và học các xu hướng trong tập huấn luyện mà không được tổng quát hoá trong tập kiểm tra. Chúng ta cần giữ cân bằng. Một trong những kĩ năng hữu ích là hiểu làm thế nào để huấn luyện trong số lượng giai đoạn phù hợp.

Để phòng ngừa hiện tượng overfitting, phương án tốt nhất là sử dụng thêm dữ liệu huấn luyện. Một mô hình được huấn luyện trên nhiều dữ liệu hơn tự nhiên sẽ được tổng quát hoá tốt hơn. Khi việc mở rộng dữ liệu huấn luyện là không thể nữa, phương án tốt tiếp theo là sử dụng cách kĩ thuật như là chính quy hoá. Việc này đặt ra những giới hạn cho số lượng và loại thông tin mà mô hình của bạn có thể lưu trữ. Nếu một mạng neuron chỉ có thể lưu trữ một số lượng nhỏ những xu hướng, quá trình tối ưu hoá sẽ bắt buộc nó phải tập trung vào những xu hướng nổi trội nhất, và tăng khả năng tổng quát hoá tốt hơn.

Trong notebook này, chúng ta sẽ khám phá hai kĩ thuật chính quy hoá phổ biến - chính quy hoá trọng số và dropout - và dùng chúng để cải thiện notebook về phân loại bình luận phim IMDB.


In [ ]:
import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__)

Tải tập dữ liệu IMDB

Thay vì sử dụng một embedding như trong notebook trước, ở đây chúng ta sẽ multi-hot encode các câu bình luận. Mô hình này sẽ nhanh chóng overfit tập huấn luyện, từ đó trình diễn được việc khi nào thì overfitting xảy ra, và làm thế nào để xử lý nó.

Multi-hot-encoding các danh sách có nghĩa là biến chúng trở thành các vectors của 0 và 1. Nói ngắn gọn, việc này biến các chuỗi [3, 5] trở thành một vector với 10,000 chiều đều bằng 0 ngoại trừ các chỉ số 3 và 5 sẽ là 1.


In [ ]:
NUM_WORDS = 10000

(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)

def multi_hot_sequences(sequences, dimension):
    # Create an all-zero matrix of shape (len(sequences), dimension)
    results = np.zeros((len(sequences), dimension))
    for i, word_indices in enumerate(sequences):
        results[i, word_indices] = 1.0  # set specific indices of results[i] to 1s
    return results


train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)

Hãy nhìn vào một vector trong những multi-hot vectors kết quả. Các chỉ số từ được sắp xếp bằng tần số, nên có thể kì vọng rằng sẽ có giá trị 1 gần chỉ số 0, như chúng ta thấy trong plot sau:


In [ ]:
plt.plot(train_data[0])

Trình diễn Overfitting

Cách đơn giản nhất để ngăn chặn overfitting là giảm kích thước của mô hình, ví dụ, số lượng tham số có thể học được trong mô hình (được xác định bởi số lớp và số đơn vị trên mỗi lớp). Trong học sâu, số lượng tham số có thể học được trong một mô hình thường được gọi là "năng lực" của mô hình. Theo trực giác, một mô hình có nhiều tham số sẽ có nhiều "khả năng ghi nhớ" hơn và do đó sẽ có thể dễ dàng học được một ánh xạ giống như từ điển hoàn hảo giữa các mẫu huấn luyện và các mục tiêu của chúng, một ánh xạ không có sức mạnh khái quát hóa, nhưng điều này sẽ vô dụng khi đưa ra dự đoán trên dữ liệu chưa từng thấy trước đây.

Chúng ta cần luôn ghi nhớ điều này: các mô hình học sâu có xu hướng phù hợp với dữ liệu đào tạo, nhưng thách thức thực sự là khái quát hóa, không phải chỉ là mô hình phù hợp.

Mặt khác, nếu mạng neuron có tài nguyên ghi nhớ hạn chế, nó sẽ không thể học được ánh xạ một cách dễ dàng. Để giảm thiểu tổn thất của nó, nó sẽ phải học các biểu diễn nén có sức mạnh dự đoán nhiều hơn. Đồng thời, nếu bạn làm cho mô hình của mình quá nhỏ, nó sẽ gặp khó khăn khi phù hợp với dữ liệu đào tạo. Cần phải có sự cân bằng giữa "quá nhiều năng lực" và "không đủ năng lực".

Thật không may, không có công thức kỳ diệu nào để xác định kích thước hoặc kiến trúc phù hợp của mô hình của bạn (về số lượng lớp hoặc kích thước phù hợp cho mỗi lớp). Bạn sẽ phải thử nghiệm bằng cách sử dụng một loạt các kiến trúc khác nhau.

Để tìm kích thước mô hình phù hợp, tốt nhất là bắt đầu với tương đối ít lớp và tham số, sau đó bắt đầu tăng kích thước của các lớp hoặc thêm các lớp mới cho đến khi bạn thấy giá trị giảm dần của các tổn thất từ thẩm định. Hãy thử điều này trên mạng phân loại đánh giá phim của chúng ta.

Chúng ta sẽ tạo một mô hình đơn giản chỉ sử dụng các lớp Dense làm đường cơ sở, sau đó tạo các phiên bản nhỏ hơn và lớn hơn và so sánh chúng.

Tạo một mô hình cơ sở


In [ ]:
baseline_model = keras.Sequential([
    # `input_shape` is only required here so that `.summary` works.
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

baseline_model.compile(optimizer='adam',
                       loss='binary_crossentropy',
                       metrics=['accuracy', 'binary_crossentropy'])

baseline_model.summary()

In [ ]:
baseline_history = baseline_model.fit(train_data,
                                      train_labels,
                                      epochs=20,
                                      batch_size=512,
                                      validation_data=(test_data, test_labels),
                                      verbose=2)

Tạo một mô hình nhỏ hơn

Hãy tạo một mô hình với ít các đơn vị ẩn hơn để so sánh với mô hình cơ sở mà chúng ta vừa tạo:


In [ ]:
smaller_model = keras.Sequential([
    keras.layers.Dense(4, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(4, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

smaller_model.compile(optimizer='adam',
                      loss='binary_crossentropy',
                      metrics=['accuracy', 'binary_crossentropy'])

smaller_model.summary()

Và huấn luyện mô hình sử dụng cùng một dữ liệu:


In [ ]:
smaller_history = smaller_model.fit(train_data,
                                    train_labels,
                                    epochs=20,
                                    batch_size=512,
                                    validation_data=(test_data, test_labels),
                                    verbose=2)

Tạo một mô hình lớn hơn

Như một bài tập, bạn có thể tạo ra một mô hình thậm chí còn lớn hơn và xem nó bắt đầu overfitting nhanh như thế nào. Tiếp theo, hãy thêm vào điểm chuẩn này một mạng neuron có dung lượng lớn hơn nhiều, nhiều hơn so với vấn đề sẽ đảm bảo:


In [ ]:
bigger_model = keras.models.Sequential([
    keras.layers.Dense(512, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(512, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

bigger_model.compile(optimizer='adam',
                     loss='binary_crossentropy',
                     metrics=['accuracy','binary_crossentropy'])

bigger_model.summary()

Và huấn luyện mô hình sử dụng cùng một dữ liệu:


In [ ]:
bigger_history = bigger_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

Vẽ biểu đồ thiệt hại giữa huấn luyện và thẩm định

Đường kẻ liền thể hiện độ thiệt hại từ huấn luyện, và đường đứt đoạn thể hiện độ thiệt hại từ thẩm định (nên nhớ rằng: độ thiệt hại từ thẩm định thấp biểu lộ một mô hình tốt hơn). Ở đây, mạng neuron nhỏ hơn bắt đầu overfit chậm hơn mô hình cơ bản (sau 6 epoch thay vì 4) và hiệu suất của nó giảm chậm hơn nhiều khi nó bắt đầu overfit.


In [ ]:
def plot_history(histories, key='binary_crossentropy'):
  plt.figure(figsize=(16,10))

  for name, history in histories:
    val = plt.plot(history.epoch, history.history['val_'+key],
                   '--', label=name.title()+' Val')
    plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
             label=name.title()+' Train')

  plt.xlabel('Epochs')
  plt.ylabel(key.replace('_',' ').title())
  plt.legend()

  plt.xlim([0,max(history.epoch)])


plot_history([('baseline', baseline_history),
              ('smaller', smaller_history),
              ('bigger', bigger_history)])

Nhận thấy rằng mạng neuron lớn hơn bắt đầu overfit gần như ngay lập tức, sau chỉ một epoch, và overfit nghiêm trọng hơn. Năng lực càng mạnh thì mạng neuron càng nhanh có thể lên mô hình cho dữ liệu huấn luyện (dẫn đến độ thiệt hại từ huấn luyện thấp), nhưng càng mẫn cảm với overfitting (dẫn đến sự khác biệt lớn giữa độ thiệt hại từ huấn luyện và thẩm định).

Các chiến lược để ngăn ngừa Overfitting

Thêm điều hoà trọng số

Có thể bạn đã quen thuộc với nguyên tắc Razor của Occam: Cho hai sự giải thích đối với một việc gì đó, sự giải thích có khả năng đúng nhất thường "đơn giản" nhất, với ít giả định nhất. Nguyên tắc này cũng ứng dụng được cho mô hình mạng neuron: Cho vài tập huấn luyện và một mạng kiến trúc, có nhiều tập giá trị trọng số (nhiều mô hình) có thể giải thích dữ liệu, và mô hình đơn giản nhất thường có ít có khả năng overfit hơn các mô hình phức tạp.

Một "mô hình đơn giản" trong ngữ cảnh này là một mô hình mà sự phân bổ của các giá trị tham số có ít entropy (hoặc một mô hình với ít tham số hơn, như chúng ta thấy trong phần trên). Như vậy một cách phổ biến để giảm overfitting là đặt các hạn chế lên sự phức tạp của mạng neuron, bằng cách bắt các trọng số chỉ được lấy các giá trị nhỏ, qua đó làm sự phân bổ giá trị trọng số được "điều hoà" hơn. Phương pháp này được gọi là "điều hoà trọng số", và nó được làm bằng việc thêm vào hàm thiệt hại của mạng neuron một chi phí đi liền với trọng số lớn. Chi phí này có hai loại:

  • L1 regularization, chi phí thêm vào tỉ lệ thuận với giá trị tuyệt đối của hệ số trọng số (được coi là "L1 norm" của các trọng số).
  • L2 regularization, chi phí thêm vào tỉ lệ thuận với bình phương của hệ số trọng số (được coi là bình phương "L2 norm" của các trọng số). Điều hoà L2 cũng được gọi là trọng số suy vi trong ngữ cảnh của mạng neuron. Đừng để các tên gọi khác nhau đánh lừa bạn: xét về toán học, trọng số suy vi chính là điều hoà L2.

Điều hoà L1 thêm sự thưa thớt để làm cho trọng số của vài tham số về zero. Điều hoà L2 bắt phạt trọng số của các tham số mà không làm thưa chúng, đây là một lý do khiến điều hoà L2 phổ biến hơn.

Trong tf.keras, điều hoà trọng số được thêm vào bằng việc bỏ điều hoà trọng số vào các lớp như các từ khoá. Bây giờ chúng ta hãy thêm điều hoà L2.


In [ ]:
l2_model = keras.models.Sequential([
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
                       activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])

l2_model.compile(optimizer='adam',
                 loss='binary_crossentropy',
                 metrics=['accuracy', 'binary_crossentropy'])

l2_model_history = l2_model.fit(train_data, train_labels,
                                epochs=20,
                                batch_size=512,
                                validation_data=(test_data, test_labels),
                                verbose=2)

l2(0.001) means that every coefficient in the weight matrix of the layer will add 0.001 * weight_coefficient_value**2 to the total loss of the network. Note that because this penalty is only added at training time, the loss for this network will be much higher at training than at test time.

Here's the impact of our L2 regularization penalty:


In [ ]:
plot_history([('baseline', baseline_history),
              ('l2', l2_model_history)])

As you can see, the L2 regularized model has become much more resistant to overfitting than the baseline model, even though both models have the same number of parameters.

Add dropout

Dropout is one of the most effective and most commonly used regularization techniques for neural networks, developed by Hinton and his students at the University of Toronto. Dropout, applied to a layer, consists of randomly "dropping out" (i.e. set to zero) a number of output features of the layer during training. Let's say a given layer would normally have returned a vector [0.2, 0.5, 1.3, 0.8, 1.1] for a given input sample during training; after applying dropout, this vector will have a few zero entries distributed at random, e.g. [0, 0.5, 1.3, 0, 1.1]. The "dropout rate" is the fraction of the features that are being zeroed-out; it is usually set between 0.2 and 0.5. At test time, no units are dropped out, and instead the layer's output values are scaled down by a factor equal to the dropout rate, so as to balance for the fact that more units are active than at training time.

In tf.keras you can introduce dropout in a network via the Dropout layer, which gets applied to the output of layer right before.

Let's add two Dropout layers in our IMDB network to see how well they do at reducing overfitting:


In [ ]:
dpt_model = keras.models.Sequential([
    keras.layers.Dense(16, activation='relu', input_shape=(NUM_WORDS,)),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(16, activation='relu'),
    keras.layers.Dropout(0.5),
    keras.layers.Dense(1, activation='sigmoid')
])

dpt_model.compile(optimizer='adam',
                  loss='binary_crossentropy',
                  metrics=['accuracy','binary_crossentropy'])

dpt_model_history = dpt_model.fit(train_data, train_labels,
                                  epochs=20,
                                  batch_size=512,
                                  validation_data=(test_data, test_labels),
                                  verbose=2)

In [ ]:
plot_history([('baseline', baseline_history),
              ('dropout', dpt_model_history)])

Adding dropout is a clear improvement over the baseline model.

To recap: here are the most common ways to prevent overfitting in neural networks:

  • Get more training data.
  • Reduce the capacity of the network.
  • Add weight regularization.
  • Add dropout.

And two important approaches not covered in this guide are data-augmentation and batch normalization.