Kiểm tra gradient bằng sai phân hữu hạn trước khi tin vào huấn luyện
Suy ra sai số sai phân trung tâm, phát hiện tín hiệu bị mất do làm tròn và lỗi gradient bị che khuất, rồi kiểm tra giới hạn của phép đối chiếu theo hướng.

Kiểm tra gradient là đối chiếu một đạo hàm với mức thay đổi của hàm vô hướng mà nó phải biểu diễn. Khi tự viết hàm mất mát hoặc phép tính lan truyền ngược, phép đối chiếu này có thể phát hiện lỗi dấu, cách gộp mất mát hoặc chỉ số trước khi huấn luyện. Một lần kiểm tra đạt chỉ xác nhận sự phù hợp tại những đầu vào và độ phân giải số đã thử.
Nếu bạn đã biết đạo hàm riêng và Python cơ bản, bài nền tảng này cung cấp một phép kiểm tra nhỏ có đáp án chính xác, rồi cố ý phá vỡ từng giả định của nó. Ta sẽ phân biệt sai số cắt cụt, thông tin bị mất khi tính bằng số dấu phẩy động, hướng kiểm tra không phát hiện được lỗi và điểm không khả vi. Các dữ liệu và hàm bên dưới đều là ví dụ được dựng để phân tích; bài không báo cáo kết quả huấn luyện mạng nơ-ron.
Bắt đầu bằng đáp án không phụ thuộc vào số dấu phẩy động
Xét f(x,y) = x³ + 3xy + 2y². Tại (1,-2), giá trị hàm là ba và gradient là (-3,-5):
g_x = 3x² + 3y
g_y = 3x + 4y
D_h(v) = [f(p + h v) - f(p - h v)] / (2h)
Với tọa độ x, hãy khai triển hạng bậc ba ở hai phía. Các lũy thừa chẵn của độ nhiễu triệt tiêu khi lấy hiệu. Kết quả chính xác là g_x + h²; còn với tọa độ y, kết quả chính xác là g_y. Sai phân tiến theo x có sai số 3xh + h². Đây là các đẳng thức đại số của đa thức đang xét, không phải ước lượng về sai số làm tròn.
Chạy lần lượt bốn khối Python trong cùng một phiên. Fraction giúp phép so sánh đầu tiên có kết quả chính xác. Ta dùng nó để học cách kiểm tra, không đề xuất biểu diễn số của một mô hình lớn theo cách này.
from fractions import Fraction as Q
def f(x, y):
return x**3 + 3*x*y + 2*y*y
def central(fun, point, direction, h):
plus = tuple(z + h*d for z, d in zip(point, direction))
minus = tuple(z - h*d for z, d in zip(point, direction))
return (fun(*plus) - fun(*minus)) / (2*h)
point = (Q(1), Q(-2))
gradient = (Q(-3), Q(-5))
for h in (Q(1, 10), Q(1, 100), Q(1, 1000)):
dx = central(f, point, (1, 0), h)
dy = central(f, point, (0, 1), h)
assert dx == gradient[0] + h*h
assert dy == gradient[1]
print(h, dx, dy)
Ba ước lượng theo x là −2,99; −2,9999 và −2,999999; mọi ước lượng theo y đều là −5. Khi hàm đủ trơn, khai triển Taylor cho sai phân trung tâm sai số bậc h², so với sai số thường có bậc h của sai phân tiến. Hạng sai số đầu có thể bằng không, như ở phần phụ thuộc bậc hai vào y trong ví dụ này. Không nên suy ra rằng mọi tham số có cùng hệ số sai số.
Tài liệu về cơ chế gradcheck của PyTorch mô tả cách dựng ma trận Jacobian của hàm giá trị thực bằng cách nhiễu đầu vào, rồi đối chiếu với phép vi phân tự động. Bài này chỉ cài đặt sai phân theo hướng cho một hàm vô hướng. Ta không chạy PyTorch và không xét đạo hàm phức.
Các đường trong hình biểu diễn sai số đa thức chính xác. Chúng không phải đường sai số làm tròn đo được, cũng không có nghĩa rằng cứ giảm bước nhiễu trong tính toán dấu phẩy động thì phép kiểm tra sẽ tốt hơn.
Một hằng số không đổi đạo hàm vẫn có thể xóa mất tín hiệu số
Cộng hằng số không làm thay đổi đạo hàm toán học. Tuy vậy, nó có thể khiến phép tính sai phân hữu hạn không còn dùng được. Trên bản Python sử dụng IEEE 754 binary64, khoảng cách giữa 2**60 và số biểu diễn kế tiếp phía trên là 256. Khi cộng giá trị nhỏ của hàm đang xét vào hằng số ấy, phép làm tròn xóa mất chênh lệch mà ta cần lấy hiệu.
import math
import sys
assert sys.float_info.radix == 2 and sys.float_info.mant_dig == 53
float_point = (1.0, -2.0)
h = 0.125
shifted = lambda x, y: 2**60 + f(x, y)
plain = central(f, float_point, (1.0, 0.0), h)
lost = central(shifted, float_point, (1.0, 0.0), h)
assert plain == -2.984375 and lost == 0.0
assert math.ulp(float(2**60)) == 256.0
print(plain, lost, math.ulp(float(2**60)))
tiny = 2**-54
assert 1.0 + tiny == 1.0 and 1.0 - tiny == 1.0
print('Both tiny perturbations round back to the input.')
Số không là kết quả ước lượng số, không phải đạo hàm của hàm toán học sau khi cộng hằng số. Trừ hằng số khỏi đầu ra đã làm tròn không thể khôi phục thông tin bị mất. Nếu hằng số không ảnh hưởng đạo hàm ấy là không cần thiết, hãy bỏ nó trước khi tính hàm phục vụ chẩn đoán; nếu không, cần xem lại độ chính xác và thang giá trị. Luôn nêu rõ khi đã thay đổi hàm dùng để kiểm tra.
Khối mã còn cho thấy một lỗi khác: chính các nhiễu của tham số cũng biến mất. Hãy kiểm tra giá trị thực sự được lưu ở phía cộng và phía trừ trước khi diễn giải một gradient bằng không hoặc có vẻ khớp hoàn hảo. Tài liệu API của PyTorch lưu ý rằng các giá trị mặc định của phép kiểm tra được thiết kế cho độ chính xác kép. Tăng độ chính xác có ích, nhưng không khiến mọi bước nhiễu hay mọi thang giá trị mục tiêu đều phù hợp.
Khớp ở một hướng vẫn có thể bỏ sót hai thành phần sai
Theo công thức trung tâm đơn giản, kiểm tra từng tọa độ cần hai lần tính hàm cho mỗi tọa độ. Kiểm tra theo một hướng chỉ cần hai lần tính hàm cho cả vector. Đổi lại, thông tin thu được hẹp hơn: ta chỉ kiểm tra tích vô hướng của gradient với hướng đã chọn.
Giả sử phép lan truyền ngược bị lỗi trả về (-2,-6) thay vì (-3,-5). Vector sai số là (1,-1). Hướng (1,1) vuông góc với sai số ấy, nên cả hai gradient đều dự đoán −8. Hướng (1,-1) mới làm lộ lỗi: dự đoán đúng là hai, còn dự đoán sai là bốn. Ta cố ý không chuẩn hóa các hướng này; phép sai phân và tích vô hướng đều dùng cùng một vector.
Với đa thức bậc ba đang xét, hạng sai số cắt cụt chính xác theo hướng là h² * v_x³. Đoạn kiểm tra sau in ước lượng trung tâm chưa hiệu chỉnh và dùng ngưỡng sai số tuyệt đối đã khai báo là 0,0001. Ngưỡng này lớn hơn rõ rệt so với sai số cắt cụt 0,000001 trong cả hai trường hợp.
wrong = (Q(-2), Q(-6))
h = Q(1, 1000)
tolerance = Q(1, 10000)
for direction in ((1, 1), (1, -1)):
numerical = central(f, point, direction, h)
correct_dot = sum(g*d for g, d in zip(gradient, direction))
wrong_dot = sum(g*d for g, d in zip(wrong, direction))
assert numerical - correct_dot == h*h*direction[0]**3
correct_passes = abs(numerical - correct_dot) <= tolerance
wrong_passes = abs(numerical - wrong_dot) <= tolerance
assert correct_passes
assert wrong_passes == (direction == (1, 1))
print(direction, numerical, correct_dot, wrong_dot, wrong_passes)
Bất kỳ sai số nào vuông góc với tất cả các hướng đã thử đều vô hình đối với phép đối chiếu theo hướng trong số học chính xác. Dùng nhiều hướng đa dạng và kiểm tra từng tọa độ giúp giảm điểm mù ấy, nhưng không chứng minh cài đặt đúng ở mọi nơi. Hãy thử đầu vào đại diện và từng nhóm tham số, kể cả vector bias nhỏ mà cách chọn mẫu đồng đều có thể hiếm khi chạm đến.
Ngay cả kiểm tra mọi tọa độ ở một đầu vào cũng có thể bỏ sót lỗi. Giả sử phép lan truyền ngược cộng thêm x-1 vào đạo hàm đúng theo x. Tại giá trị x ban đầu bằng một, hạng thừa ấy biến mất. Tại (0,-2), đạo hàm đúng theo x là −6 còn hàm bị lỗi trả về −7; bước sai phân trung tâm đủ nhỏ sẽ làm lộ chênh lệch. Hãy giữ thêm một đầu vào làm thay đổi các hạng đang đóng góp, thay vì xem một ví dụ đạt là bằng chứng cho toàn miền đầu vào.
Khi độ lớn gradient khác nhau nhiều, một quy tắc được khai báo như abs(a-n) <= atol + rtol * max(abs(a), abs(n)) kết hợp ngưỡng tuyệt đối và tương đối. Áp dụng theo từng thành phần, giữ lại các giá trị thực tế và chọn ngưỡng phù hợp với thang số. Một thành phần đúng nhưng rất lớn có thể che khuất thành phần sai nhỏ hơn trong chuẩn của cả vector. Hai giá trị cùng bằng không cũng không cung cấp thông tin nếu đầu vào chưa thực sự thay đổi.
Giữ nguyên hàm và nhận biết điểm gãy
Ghi chú kiểm tra gradient của CS231n nhấn mạnh sai phân trung tâm, độ chính xác, đầu vào đại diện và việc kiểm soát dropout hoặc tăng cường dữ liệu. Nguyên tắc cần giữ là đối chiếu cùng một hàm. Cố định dữ liệu, các lựa chọn ngẫu nhiên và trạng thái có thể thay đổi cho lần tính phía cộng, phía trừ và lần tính đạo hàm. Chỉ đặt lại seed không khôi phục thống kê tích lũy, cũng không bảo đảm hai nhánh khác nhau sử dụng các số ngẫu nhiên theo cùng cách.
Để thấy rõ ảnh hưởng, cộng 0,01 vào lần gọi phía dương và −0,01 vào lần gọi phía âm. Ngoài sai số cắt cụt, ước lượng đạo hàm nhận thêm sai số 0.01/h. Với h=0.001, riêng chênh lệch giả lập ấy đã đóng góp mười. Hai độ lệch cộng bằng nhau triệt tiêu trong số học chính xác; mặt nạ dropout cố định phức tạp hơn và cũng phải được dùng trong lần tính đạo hàm cần kiểm tra.
ReLU tạo ra một ranh giới khác. Tại không, hàm không có đạo hàm thông thường, nhưng sai phân đối xứng luôn trả về một nửa với mọi bước dương. Tại −0,25, đạo hàm bằng không, nhưng bước bằng một đi qua điểm gãy và trả về 0,375. Bước 0,125 nằm hoàn toàn trong đoạn phẳng và trả về không.
relu = lambda x: max(Q(0), x)
assert central(relu, (Q(0),), (1,), Q(1, 10)) == Q(1, 2)
assert central(relu, (Q(-1, 4),), (1,), Q(1)) == Q(3, 8)
assert central(relu, (Q(-1, 4),), (1,), Q(1, 8)) == 0
h = Q(1, 1000)
unequal_offsets = (Q(1, 100) - Q(-1, 100)) / (2*h)
assert unequal_offsets == 10
print('ReLU:', Q(1, 2), Q(3, 8), 0)
print('Extra error from unequal offsets:', unequal_offsets)
Giá trị lan truyền ngược mà thư viện chọn ở điểm gãy không nhất thiết trùng thương sai phân đối xứng ấy. Hãy kiểm tra các vùng trơn và kiểm tra riêng quy ước tại ranh giới đã được tài liệu hóa. PyTorch nêu rõ trường hợp đầu vào không khả vi và tensor có vùng nhớ chồng lấn; các tọa độ dùng chung ô nhớ không phải những nhiễu độc lập. Ví dụ vô hướng của bài không có bộ nhớ dùng chung hay lớp mang trạng thái.
Để luyện tập, hãy dùng hướng (2,1). Suy ra hạng cắt cụt chính xác 8h², đạo hàm theo hướng đúng −11 và giá trị do gradient sai dự đoán là −10, rồi chạy lại phép đối chiếu. Tiếp theo, đổi hệ số của hạng bậc ba thành hai và suy ra sai số theo tọa độ 2h² trước khi đổi câu lệnh kiểm tra. Cuối cùng, kiểm tra riêng cách gộp mất mát mà ứng dụng yêu cầu: một hàm tổng được lấy đạo hàm đúng vẫn có thể vượt qua mọi kiểm tra gradient dù ứng dụng cần giá trị trung bình. Hàm thuận và đạo hàm khớp nhau không có nghĩa rằng hàm thuận đã biểu diễn đúng bài toán cần giải.