Backpropagation từ đầu: Động cơ trước, rồi đến mạng
Viết autodiff engine 120 dòng bằng Python thuần, đối chiếu với PyTorch đến 16 chữ số thập phân và hiểu zero_grad bằng cách xóa nó.
Trên trang này
Sau bốn chương, vẫn còn một khoảng trống ở giữa khóa học.
Chương 3 đã cho chúng ta gradient descent: để cải thiện một tham số, hãy tìm độ dốc của hàm mất mát theo tham số đó rồi bước xuống dốc. Chương 4 đã cho chúng ta một hàm mất mát đáng để đi xuống. Nhưng trong cả hai chương, đạo hàm đều được tính bằng tay — một mô hình, một tham số, một dòng giải tích, và tất cả vừa trên một trang.
Giờ hãy xếp chồng hai lớp. Đầu ra của lớp thứ nhất đi vào lớp thứ hai, nên mọi trọng số trong lớp thứ nhất ảnh hưởng đến hàm mất mát thông qua mọi nơ-ron trong lớp thứ hai. Một mạng có hai lớp ẩn, mỗi lớp một trăm đơn vị, có khoảng hai mươi nghìn tham số, và mỗi tham số cần đạo hàm riêng của cùng một hàm mất mát. Làm việc đó bằng tay không phải là tẻ nhạt; nó là bất khả thi, và vẫn bất khả thi với mọi kiến trúc trong phần còn lại của khóa học này.
Lối ra không phải là một ký hiệu tốt hơn. Đó là nhận ra rằng đạo hàm của một hợp hàm có thể được tính một cách cơ học, bởi một chương trình, từ chính cấu trúc của phép tính — và nếu bạn làm theo đúng hướng, bạn có được toàn bộ hai mươi nghìn đạo hàm với chi phí xấp xỉ việc tính hàm mất mát một lần.
Cơ chế đó là reverse-mode automatic differentiation. Khi áp dụng cho mạng nơ-ron, nó được gọi là backpropagation, và đến cuối chương này bạn sẽ tự viết một bản khoảng 120 dòng Python không dùng thư viện, kiểm tra nó với PyTorch, rồi dùng nó để giải bài toán XOR đã đánh bại perceptron ở Chương 1.
Trước hết: vì sao bắt buộc phải có phi tuyến
Liên kết đến mục: Trước hết: vì sao bắt buộc phải có phi tuyếnTrước khi xây cỗ máy, cần chốt một câu hỏi, vì nếu câu trả lời đi theo hướng ngược lại thì sẽ chẳng có gì để xây.
Perceptron thất bại với XOR vì một đường thẳng không thể tách bốn điểm. Cách sửa hiển nhiên là xếp chồng: đưa đầu vào qua một lớp tuyến tính, rồi một lớp nữa. Cách đó có giúp gì không?
Không, và chứng minh chỉ mất hai dòng. Một lớp tuyến tính là . Đưa nó vào một lớp khác, , rồi thay vào:
Hợp thành là với và . Một chồng các lớp tuyến tính chỉ là một lớp tuyến tính duy nhất. Mười lớp, một nghìn lớp: vẫn là một đường thẳng, vẫn không làm được XOR.
Đáng để nhìn tận mắt điều đó xảy ra thay vì chỉ tin nó:
import numpy as np
rng = np.random.default_rng(0)
W1, b1 = rng.normal(size=(3, 2)), rng.normal(size=3)
W2, b2 = rng.normal(size=(1, 3)), rng.normal(size=1)
x = rng.normal(size=2)
two_layers = W2 @ (W1 @ x + b1) + b2
one_layer = (W2 @ W1) @ x + (W2 @ b1 + b2)
print(two_layers[0], one_layer[0], abs(two_layers[0] - one_layer[0]))-4.612963371048 -4.612963371048 0.00e+00Không phải xấp xỉ bằng nhau. Giống hệt từng bit, vì đó là cùng một phép tính được sắp xếp lại.
Vì vậy độ sâu tự nó không mua được gì. Thứ có ích là đặt một hàm phi tuyến giữa các lớp — và đó là toàn bộ lý do các hàm kích hoạt tồn tại. Chúng không phải một nét trang trí sinh học hay một mẹo chuẩn hóa. Không có chúng, lớp thứ hai chỉ là đồ trang trí.
Quy tắc dây chuyền, trên giấy, với một nút dùng chung
Liên kết đến mục: Quy tắc dây chuyền, trên giấy, với một nút dùng chungGiờ đến phần toán học, và đó là một quy tắc bạn đã biết được áp dụng ở một nơi hơi lạ.
Quy tắc dây chuyền một biến nói rằng nếu phụ thuộc vào và phụ thuộc vào , thì . Các đạo hàm nhân với nhau dọc theo một chuỗi.
Phần quan trọng ở đây là điều xảy ra khi một biến cấp dữ liệu cho nhiều hơn một đường downstream. Nếu ảnh hưởng đến thông qua và cũng thông qua , các phần đóng góp sẽ cộng lại:
Nhân dọc theo một đường, cộng qua các đường. Đó là toàn bộ backpropagation, và mọi chi tiết triển khai trong phần còn lại của chương này — bao gồm += trong code và lệnh gọi zero_grad() làm vấp ngã mọi người khi viết training loop đầu tiên — đều là hệ quả trực tiếp của từ thứ hai đó.
Lấy một mạch cụ thể gồm năm phép toán, với và :
Lưu ý rằng xuất hiện ba lần: trong , trong , và trực tiếp trong . Hãy làm backward pass trên giấy, từ phải sang trái, bắt đầu từ :
Qua phép cộng
Liên kết đến mục: Qua phép cộng, nên và đường trực tiếp đóng góp . Phép cộng phân phối gradient đi vào không đổi đến cả hai đầu vào.
Qua tanh
Liên kết đến mục: Qua tanhvới , nên .
Qua phép nhân
Liên kết đến mục: Qua phép nhân, nên và . Phép nhân hoán đổi: gradient của mỗi đầu vào được nhân theo tỉ lệ bởi đầu vào kia.
Gom ba đường vào x
Liên kết đến mục: Gom ba đường vào xQua : . Qua : . Trực tiếp: .
Hãy giữ con số đó. Vài trang nữa, một chương trình sẽ tạo ra nó mà không được cho biết bất kỳ điều nào ở trên.
Xây engine
Liên kết đến mục: Xây engineTrực giác khiến việc này lập trình được là: mỗi bước trong số đó đều cục bộ. Để đẩy gradient qua nút phép nhân, bạn cần gradient đi vào và hai giá trị đầu vào đã lưu — không cần biết gì về phần còn lại của mạch. Mỗi phép toán biết cách tự lấy đạo hàm của chính nó.
Vì vậy hãy tạo một con số nhớ được điều gì đã tạo ra nó.
class Value:
"""A number that remembers where it came from."""
def __init__(self, data, _children=(), _op=""):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._prev = set(_children)
self._op = _opBốn trường. data là giá trị. grad tích lũy . _prev là tập các Value mà giá trị này được tính từ đó — các cạnh của đồ thị. Và _backward là một closure mà mỗi phép toán cài vào: nó biết cách đẩy gradient của nút này lùi một bước về các đầu vào.
Mọi toán tử đều theo cùng một hình dạng: tính đầu ra, ghi lại cha mẹ, cài quy tắc cục bộ.
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), "+")
def _backward():
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), "*")
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out
def tanh(self):
t = math.tanh(self.data)
out = Value(t, (self,), "tanh")
def _backward():
self.grad += (1 - t * t) * out.grad
out._backward = _backward
return out
def relu(self):
out = Value(self.data if self.data > 0 else 0.0, (self,), "relu")
def _backward():
self.grad += (1.0 if out.data > 0 else 0.0) * out.grad
out._backward = _backward
return outĐọc bốn thân _backward như một bảng, bạn sẽ thấy các mẫu dòng chảy từ phần suy diễn trên giấy nằm ngay đó:
| phép toán | nó làm gì với gradient |
|---|---|
+ | phân phối — cùng một gradient đến mọi đầu vào |
* | hoán đổi — mỗi đầu vào được nhân theo tỉ lệ bởi giá trị của đầu vào kia |
relu | định tuyến — cho đi qua hoặc chặn hoàn toàn |
tanh | làm suy giảm — nhân theo tỉ lệ bởi , giá trị tối đa là 1 và thường nhỏ hơn |
Từng dòng đều dùng += và không bao giờ dùng =. Đó là quy tắc “cộng qua các đường”, được mã hóa. Một nút cấp dữ liệu cho hai consumer sẽ được gọi hai lần, và hai phần đóng góp tự cộng lại.
Rồi đến driver, phần duy nhất có hiểu biết toàn cục:
def backward(self):
order, seen = [], set()
def build(v):
if v in seen:
return
seen.add(v)
for child in v._prev:
build(child)
order.append(v)
build(self)
self.grad = 1.0
for v in reversed(order):
v._backward() build tạo ra một thứ tự tô-pô của đồ thị: mọi nút xuất hiện sau toàn bộ đầu vào của nó. Đi qua danh sách đó theo chiều ngược lại đảm bảo rằng khi bạn gọi _backward của một nút, gradient của chính nút đó đã hoàn chỉnh — mọi consumer downstream của nó đã đóng góp xong. Sai thứ tự là bạn đẩy một gradient mới hoàn thành một nửa ngược về sau, tạo ra đáp án sai mà không có thông báo lỗi.
Nó có khớp với phần làm trên giấy không?
Liên kết đến mục: Nó có khớp với phần làm trên giấy không?x = Value(0.5)
y = Value(1.4)
a = x * y
b = x + y
c = a * b
d = c.tanh()
L = d + x
L.backward()
print(x.grad, y.grad)forward: a=0.7000 b=1.9000 c=1.3300 d=0.8692 L=1.3692
backward: dL/dd=1.0000 dL/dc=0.2444 dL/da=0.4644 dL/db=0.1711
dL/dx=1.8212 dL/dy=0.40331.8212. Cùng một con số, từ một chương trình chỉ được cho biết quy tắc của +, quy tắc của *, quy tắc của tanh, và không biết gì về mạch này.
Hai kiểm tra độc lập, vì “nó khớp với thứ tôi tự suy ra” là một bài kiểm tra yếu khi cùng một người làm cả hai.
Lấy đạo hàm số. Nhích đầu vào và đo. Sai phân trung tâm ước lượng đạo hàm mà không cần giải tích nào cả:
dL/dx: analytic=1.821202805 numeric=1.821202805 |diff|=1.80e-10
dL/dy: analytic=0.403269235 numeric=0.403269235 |diff|=7.64e-12So với PyTorch, có một autodiff engine công nghiệp do những người làm việc này để kiếm sống viết ra:
torch dL/dx=1.821202805316 ours=1.821202805316 |diff|=2.22e-16
torch dL/dy=0.403269234753 ours=0.403269234753 |diff|=1.11e-16Khớp ở mức , tức machine epsilon của số thực 64-bit: hai engine đang thực hiện cùng một phép tính số học. Hãy giữ phép kiểm tra số trong túi — đó là công cụ để gỡ lỗi backward pass của một lớp mới, và là lý do một gradient sai có thể được tìm thấy.
Bão hòa, được đo
Liên kết đến mục: Bão hòa, được đoCùng một mạch, đầu vào khác. Đặt và , khiến :
x=0.5, y=1.4: dL/dc = 0.244400 three paths into x: 0.6501 + 0.1711 + 1.0000 = 1.8212
x=2.0, y=-3.0: dL/dc = 0.000025 three paths into x: 0.0001 + -0.0001 + 1.0000 = 0.9999Gradient đi qua nút giảm đi 9.945 lần. Mọi thứ upstream của nó — trong một mạng thật, mọi lớp trước nó — gần như không nhận được gì. Hai đường qua mạch đã im lặng; chỉ kết nối trực tiếp bỏ qua vẫn còn mang tín hiệu.
Đó là vấn đề vanishing gradient, trong một nút. Xếp bốn mươi lớp và nhân bốn mươi hệ số như vậy với nhau, các lớp đầu sẽ ngừng học hoàn toàn. Nhân tiện, đây cũng là một lập luận ủng hộ skip connections mà bạn có thể thấy ở dạng thu nhỏ: đường đi vòng qua phi tuyến là đường duy nhất sống sót.
zero_grad thực sự làm gì, và vì sao lỗi này ẩn mình
Liên kết đến mục: zero_grad thực sự làm gì, và vì sao lỗi này ẩn mìnhMọi _backward đều dùng +=. Điều đó đúng — đó là cách các đường cộng lại. Nhưng nó có một hệ quả bắt tất cả mọi người: gradient cũng tích lũy qua các lần gọi backward(). Engine không hề biết lần gọi thứ hai của bạn là một bước huấn luyện mới chứ không phải một đường khác trong cùng đồ thị.
Vì vậy training loop phải xóa chúng:
for step in range(steps):
ys = [model(x) for x, _ in DATA]
loss = sum((yp - yt) ** 2 for yp, (_, yt) in zip(ys, DATA))
for p in model.parameters():
p.grad = 0.0
loss.backward()
for p in model.parameters():
p.data -= lr * p.gradĐây là optimizer.zero_grad() trong PyTorch, và lời khuyên thường gặp là quên nó sẽ làm hỏng huấn luyện. Vậy hãy xóa hai dòng đó và xem nó hỏng đến mức nào. Cùng seed, cùng mọi thứ, 200 bước XOR:
| learning rate | seed | có reset | không reset |
|---|---|---|---|
| 0.05 | 1337 | loss 3.255088, 3/4 | loss 0.000000, 4/4 |
| 0.05 | 7 | loss 2.144820, 2/4 | loss 0.000000, 4/4 |
| 0.05 | 42 | loss 2.126074, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 1337 | loss 0.038597, 4/4 | loss 0.000000, 4/4 |
| 0.1 | 7 | loss 2.055048, 2/4 | loss 0.000000, 4/4 |
| 0.1 | 42 | loss 2.049876, 2/4 | loss 0.000073, 4/4 |
| 0.3 | 1337 | loss 4.512310, 2/4 | loss 8.000000, 2/4 |
| 0.3 | 7 | loss 0.015247, 4/4 | loss 4.000000, 3/4 |
| 0.3 | 42 | loss 0.005478, 4/4 | loss 4.000000, 3/4 |
Ở các learning rate nhỏ, phiên bản có lỗi thắng từng hàng một. Nó hội tụ khi phiên bản đúng bị kẹt.
Đó không phải là may mắn ngẫu nhiên, và rất đáng hiểu, vì nó giải thích vì sao lỗi này khó bắt đến vậy. Nếu bạn không bao giờ xóa gradient, thì ở bước tham số được cập nhật bằng tổng của mọi gradient đã tính từ trước đến nay. Trên một hàm mất mát liên tục chỉ về gần cùng một hướng, tổng đó tăng đều, và hiệu ứng là learning rate tự tăng lên. Ở , nơi thuật toán đúng đang bò, step size runaway trông đúng như một bản sửa lỗi.
Rồi nhìn ba hàng cuối. Ở , cùng cơ chế đó làm mô hình vỡ tung — loss 8.0 là điểm số của một mô hình sụp về hằng số — bằng một nửa của 16 mà bốn câu trả lời sai tối đa sẽ gây ra — — trong khi phiên bản đúng lúc này hội tụ sạch sẽ.
Vì vậy phát biểu trung thực không phải là “luôn gọi zero_grad nếu không mô hình của bạn sẽ không huấn luyện”. Mà là: không có nó, bạn không còn chạy gradient descent nữa. Bạn đang chạy một thứ có step size trôi dần lên với tốc độ không ai chọn, và nó sẽ có vẻ hoạt động, đôi khi còn tốt hơn thứ thật, cho đến khi không còn như vậy — khi đó bạn sẽ đổ lỗi cho learning rate, khởi tạo, hoặc dữ liệu. Đây là hình dạng của những lỗi tệ nhất trong machine learning: chúng không crash, chúng biến thuật toán thành một thuật toán khác đôi khi đạt điểm tốt hơn.
Mạng, và cuối cùng là XOR
Liên kết đến mục: Mạng, và cuối cùng là XORKhi engine đã xong, một mạng nơ-ron hầu như không cần nhiều code. Một nơ-ron là một dot product, một bias và một activation; một lớp là danh sách các nơ-ron; một mạng là danh sách các lớp.
class Neuron:
def __init__(self, nin):
self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]
self.b = Value(0.0)
def __call__(self, x):
act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)
return act.tanh()
def parameters(self):
return self.w + [self.b]
class Layer:
def __init__(self, nin, nout):
self.neurons = [Neuron(nin) for _ in range(nout)]
def __call__(self, x):
out = [n(x) for n in self.neurons]
return out[0] if len(out) == 1 else out
def parameters(self):
return [p for n in self.neurons for p in n.parameters()]
class MLP:
def __init__(self, nin, nouts):
sizes = [nin] + nouts
self.layers = [Layer(sizes[i], sizes[i + 1]) for i in range(len(nouts))]
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
def parameters(self):
return [p for layer in self.layers for p in layer.parameters()]Không có backward pass nào trong tất cả những thứ đó. Không một dòng. Lớp Value đã biết cách lấy đạo hàm của bất cứ thứ gì các lớp này tình cờ xây ra, và đó là mục đích của việc viết nó trước: một autodiff engine không biết nó đang được dùng cho mạng nơ-ron.
Giờ đến bài toán từ Chương 1. Hai đầu vào, hai đơn vị ẩn, một đầu ra, chín tham số:
step 1: loss 4.156690
step 10: loss 4.005572
step 50: loss 3.996708
step 100: loss 3.510700
step 200: loss 0.038597
[0, 0] -> -0.9081 (target -1) ok
[0, 1] -> +0.8934 (target +1) ok
[1, 0] -> +0.8906 (target +1) ok
[1, 1] -> -0.9207 (target -1) okBốn trên bốn. Hàm mà không perceptron nào tính được — đã được chứng minh ở Chương 1 bằng bốn bất đẳng thức đòi vừa dương vừa âm — được tính bởi chín con số tìm được tự động.
Lớp ẩn đã làm gì
Liên kết đến mục: Lớp ẩn đã làm gìPhần thỏa mãn không phải là nó hoạt động. Mà là có thể nhìn thấy cách nó hoạt động, vì với hai đơn vị ẩn, biểu diễn trung gian là một điểm trong mặt phẳng và bạn có thể chỉ việc in ra.
Sau khi huấn luyện đến loss 0.001241, đây là nơi mỗi đầu vào hạ cánh sau lớp ẩn, và điều nơ-ron đầu ra làm với nó:
| đầu vào | đầu ra lớp ẩn | điểm đầu ra | nhãn |
|---|---|---|---|
Nhìn vào hàng đầu và hàng thứ tư. Các đầu vào và là hai góc đối diện theo đường chéo của hình vuông — xa nhau nhất có thể trong bài toán này — và lớp ẩn ánh xạ chúng tới và . Gần như cùng một điểm. Lớp này đã gấp mặt phẳng để hai góc bị loại đặt chồng lên nhau, và khi chúng ở cùng một chỗ, một đường thẳng có thể tách chúng khỏi hai điểm còn lại.
Và nơ-ron đầu ra chính xác là đường thẳng đó. Các tham số đã học của nó là , , nên ranh giới quyết định của nó là
đó là một đường thẳng — một perceptron, cùng đối tượng từ Chương 1, không thay đổi. Khi đó nó không giải được XOR và bây giờ cũng không. Điều thay đổi là nó không còn nhìn vào đầu vào nữa; nó đang nhìn vào một không gian mà lớp đầu tiên xây cho nó, trong đó bài toán có thể tách tuyến tính.
Đó là một biểu diễn đã học, và đáng để nói chính xác vì cụm từ này sẽ được dùng khá lỏng trong phần còn lại của khóa học, và trong phần còn lại của lĩnh vực. Nó không phải là nén, tóm tắt, hay embedding theo nghĩa huyền bí nào. Nó là một phép đổi tọa độ, được học thay vì được thiết kế, với nhiệm vụ duy nhất là làm cho công việc của lớp tiếp theo trở nên dễ dàng.
Định lý xấp xỉ phổ quát, và điều nó không nói
Liên kết đến mục: Định lý xấp xỉ phổ quát, và điều nó không nóiCó một định lý ở đây, và nó thường bị trích dẫn rất tệ.
Cybenko năm 1989 và Hornik năm 1991 đã chứng minh rằng một mạng feedforward với một lớp ẩn duy nhất và một hàm kích hoạt phù hợp có thể xấp xỉ bất kỳ hàm liên tục nào trên một tập compact, tới bất kỳ độ chính xác nào bạn muốn, miễn là có đủ đơn vị ẩn.34 Đây là một kết quả thật sự quan trọng: nó nói kiến trúc không phải là giới hạn.
Giờ hãy đọc những gì nó bỏ qua. Nó không nói cần bao nhiêu đơn vị — cận có thể lớn đến mức thiên văn. Nó không nói các trọng số có thể được tìm thấy; nó khẳng định sự tồn tại, và gradient descent từ một khởi đầu ngẫu nhiên không phải là oracle. Và nó không nói gì về hành vi trên dữ liệu bạn chưa thấy, vốn là nửa sau của Chương 6.
Khoảng cách giữa “tồn tại” và “có thể tìm được” không phải chuyện hàn lâm. Đây là cùng bài toán XOR, 50 lần khởi tạo ngẫu nhiên mỗi cấu hình, 1000 bước, chỉ thay đổi kích thước lớp ẩn:
| đơn vị ẩn | số lần khởi tạo đạt 4/4 |
|---|---|
| 2 | 38 / 50 (76 %) |
| 3 | 49 / 50 (98 %) |
| 4 | 50 / 50 (100 %) |
| 8 | 47 / 50 (94 %) |
Với kiến trúc tối thiểu có thể dùng được, cứ bốn lần chạy thì một lần không bao giờ tới đích — nó ổn định trong một cấu hình không thể đi xuống thoát ra, chính là cực tiểu cục bộ mà Chương 3 đã cho thấy trên một bề mặt một chiều. Thêm một đơn vị và các thất bại gần như biến mất, không phải vì mạng trở nên biểu đạt hơn (hai đơn vị đã đủ — 38 lần chạy chứng minh điều đó) mà vì các chiều bổ sung cho phép quá trình đi xuống có nhiều hướng hơn để thoát.
Và rồi tám đơn vị lại tệ hơn bốn một chút. Với learning rate và ngân sách bước cố định, nhiều capacity hơn không tốt hơn một cách đơn điệu. Bất kỳ ai nói với bạn rằng cách sửa một mạng bị kẹt luôn là mạng lớn hơn thì đang ngoại suy từ phần giữa của bảng đó.
Đây cũng là bài học của định lý hội tụ trong Chương 1, và sẽ là cùng bài học trong Chương 10 về scaling laws, dưới dạng mà chương đó đưa ra: dự đoán loss không phải là dự đoán capability bạn đang trả tiền để có, và khoảng cách giữa hai thứ đó là nơi công việc engineering diễn ra.
Hiện chi tiết
Tùy chọn: dạng ma trận, và vì sao code phía trên không dùng nó.
Mọi thứ ở đây được viết từng scalar một, đó là cách rõ nhất để thấy cơ chế và cũng là cách chậm nhất để chạy. Trong thực tế, một lớp là một phép nhân ma trận, và backward pass của là
Các phép chuyển vị không phải là một mẹo để nhớ; chúng là hình dạng của quy tắc cộng qua các đường khi các đường được đánh chỉ số bởi các phần tử ma trận. Đối tượng tổng quát là Jacobian, ma trận của mọi đạo hàm riêng của mọi đầu ra theo mọi đầu vào, và reverse mode chính xác là phép tính một vector-Jacobian product mà không bao giờ dựng Jacobian — điều này quan trọng, vì với một lớp có 4096 đầu vào và 4096 đầu ra, ma trận đó có mười sáu triệu phần tử và không bao giờ đáng để xây.
Bạn không cần bất kỳ điều nào trong phần này để theo dõi các chương tiếp theo; phiên bản scalar làm mọi thứ phiên bản ma trận làm, chỉ chậm hơn. Nó trở nên cần thiết ở Chương 9, nơi các shape không còn hiển nhiên.
Tiếp theo là gì
Liên kết đến mục: Tiếp theo là gìGiờ bạn đã có một mạng có thể huấn luyện. Thành tựu đó nhỏ hơn cảm giác của nó, vì mạng của bạn huấn luyện trên bốn ví dụ và cũng được đo trên chính bốn ví dụ đó.
Chạy cùng code trên một tập dữ liệu thật, một loạt vấn đề mới sẽ xuất hiện, không vấn đề nào nói về gradient. Loss giảm một lúc rồi dừng. Hoặc nó giảm trên dữ liệu huấn luyện và tăng trên mọi thứ khác. Hoặc nó không nhúc nhích ngay từ bước đầu tiên, và nguyên nhân hóa ra là phạm vi của các trọng số ngẫu nhiên ban đầu. Hoặc đầu vào của một đơn vị trôi sang âm trên mọi ví dụ ở epoch thứ ba và nó đã chết kể từ đó, âm thầm, mang theo một mảnh capacity của mô hình.
Đây không phải các lỗi kỳ lạ; chúng là trạng thái bình thường của một mạng vừa được viết ra, và không lỗi nào tự thông báo. Gradient đúng — bạn đã kiểm tra nó với PyTorch tới mười sáu chữ số thập phân — và mô hình vẫn không học.
Chương 6 nói về điều đó: khởi tạo, chuẩn hóa, overfitting và regularization, cùng thói quen chẩn đoán là hỏi điều nào đang xảy ra trước khi thay đổi bất cứ thứ gì. Đó là khác biệt giữa một mạng chạy được và một mạng hoạt động được.
Nguồn và phương pháp
Liên kết đến mục: Nguồn và phương phápLớp Value trong chương này xuất phát trực tiếp từ micrograd của Andrej Karpathy, và video của anh The spelled-out intro to neural networks and backpropagation: building micrograd là ba giờ đáng giá nhất bạn có thể dành cho tài liệu này nếu muốn nghe một người khác giải thích theo cách thứ hai. Bài viết năm 2016 của anh Yes you should understand backprop lập luận cho việc tự viết một bản, và là bài đọc được giao trong Stanford CS224n. Ghi chú CS231n về backpropagation (cs231n.github.io/optimization-2) là cách trình bày kinh điển về các mẫu dòng chảy được lập bảng phía trên. Với phần toán học như giải tích trên đồ thị thay vì giai thoại mạng nơ-ron, chương 5.6 của Mathematics for Machine Learning của Deisenroth, Faisal và Ong rõ ràng một cách hiếm có; còn khảo sát Automatic Differentiation in Machine Learning: a Survey của Baydin, Pearlmutter, Radul và Siskind (arXiv:1502.05767) là tài liệu tham chiếu cho toàn bộ lĩnh vực, bao gồm trade-off forward/reverse đã thảo luận ở trên.
Tài liệu tham khảo
Liên kết đến mục: Tài liệu tham khảo-
Linnainmaa, S. The representation of the cumulative rounding error of an algorithm as a Taylor expansion of the local rounding errors. Luận văn thạc sĩ, University of Helsinki (1970). Reverse-mode accumulation, mười sáu năm trước khi nó đến lĩnh vực này và dưới một động cơ hoàn toàn khác. ↩
-
Rumelhart, D. E., Hinton, G. E. and Williams, R. J. Learning representations by back-propagating errors. Nature 323, tr. 533–536 (1986). Bài báo đã khiến phương pháp này được biết đến, và là nguồn của cách đọc các đơn vị ẩn như các biểu diễn đã học mà mục Lớp ẩn đã làm gì của chương này dành phần đo đạc để khảo sát. ↩
-
Cybenko, G. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals and Systems 2, tr. 303–314 (1989). ↩
-
Hornik, K. Approximation capabilities of multilayer feedforward networks. Neural Networks 4(2), tr. 251–257 (1991). Tổng quát hóa Cybenko: kết quả phụ thuộc vào việc activation không phải đa thức, chứ không phụ thuộc vào việc nó có dạng sigmoidal. ↩