0
0
Lập trình
Admin Team
Admin Teamtechmely

Chuyển đổi micrograd sang C++: Học lại từ đầu

Đăng vào 1 tháng trước

• 7 phút đọc

Chuyển đổi micrograd sang C++: Bước đầu tiên để trở lại với cơ bản

Hãy để tôi thẳng thắn: tôi không phát minh ra điều gì ở đây. Đây là phiên bản micrograd tuyệt vời của Andrej Karpathy được chuyển sang C++, không hơn không kém. Nhưng đôi khi, cách tốt nhất để thực sự hiểu một điều gì đó là tái xây dựng nó bằng một ngôn ngữ khác, và đó chính xác là những gì tôi cần.

Tại sao tôi quay lại với cơ bản

Tình huống của tôi là: Tôi đã làm việc như một kỹ sư phần mềm trong lĩnh vực tự động hóa điện tử, chủ yếu làm việc với các thư viện cấp cao. PyTorch ở đây, TensorFlow ở đó, chỉ cần cắm và chạy. Nhưng tôi nhận ra rằng mình đã trở nên quá thoải mái; có thể là quá thoải mái. Kỹ năng C++ của tôi đang trở nên cùn đi, và quan trọng hơn, tôi đã xem mạng nơ-ron như những chiếc hộp đen ma thuật.

Vì vậy, tôi quyết định lùi lại một bước. Rất xa. Trở lại với "hãy để tôi thực hiện phân biệt tự động từ đầu".

Dự án đầu tiên trong lộ trình "quay lại với cơ bản" của tôi:

  1. Bước 1: Chuyển đổi micrograd sang C++ (làm mới kỹ năng C++, hiểu sâu về backpropagation)
  2. Bước 2: làm quen với thống kê và học máy xác suất, sau đó triển khai Machine Learning có thông tin vật lý (PIML) trong Python
  3. Bước 3: tốt, hãy xem tôi sẽ kết thúc ở đâu.

Tôi sẽ cố gắng ghi lại hành trình này bởi vì, có thể tôi không phải là người duy nhất cần quay lại với cơ bản.

Những gì micrograd đã dạy tôi (Và C++ đã thêm vào)

Nếu bạn chưa xem micrograd của Karpathy, hãy dừng lại và đi xem bài giảng của ông trước. Thực sự đấy. Nó là một động cơ phân biệt tự động 150 dòng có thể đào tạo các mạng nơ-ron, và đó là vàng giáo dục thuần khiết.

Thiên tài của micrograd không nằm ở sự phức tạp; nó nằm ở sự đơn giản. Mỗi phép toán (cộng, nhân, tanh) biết cách tính đạo hàm của chính nó. Kết hợp chúng lại với nhau, và bạn nhận được backpropagation miễn phí.

Việc chuyển đổi nó sang C++ buộc tôi phải suy nghĩ về những điều mà Python che giấu:

Quản lý bộ nhớ

Bộ thu gom rác của Python tự động quản lý các tham chiếu vòng trong các đồ thị tính toán. Trong C++, tôi phải rõ ràng về shared_ptrweak_ptr để tránh rò rỉ bộ nhớ.

cpp Copy
// Điều này mất nhiều thời gian hơn tôi muốn thừa nhận :")
template<typename T>
class Value {
    T data;
    double grad;
    std::vector<std::shared_ptr<Value<T>>> children;
    std::function<void(Value<T>&)> _backward;
    // ... nhiều quản lý con trỏ cẩn thận :->
};

Tính an toàn kiểu dữ liệu

Kiểu động của Python cho phép bạn lười biếng. Các template C++ buộc tôi phải suy nghĩ rõ ràng về những gì mà lớp Value của tôi nên hỗ trợ. Hóa ra, việc rõ ràng giúp bạn phát hiện lỗi sớm hơn.

Các yếu tố hiệu suất

Không phải là triển khai nhỏ này là quan trọng về hiệu suất, nhưng C++ khiến tôi phải suy nghĩ về các ngữ nghĩa di chuyển, các bản sao không cần thiết, và cách bố trí bộ nhớ theo những cách mà Python trừu tượng hóa.

Những khoảnh khắc "Ahaaaaa" (và những lỗi gây cười)

Lỗi tích lũy đạo hàm

Triển khai đầu tiên của tôi đã có một lỗi tinh vi nơi mà các đạo hàm cứ tiếp tục tích lũy giữa các lần huấn luyện. Mất một thời gian dài để nhận ra rằng tôi đã không đặt đạo hàm về 0 giữa các lần huấn luyện.

cpp Copy
// Lỗi đã làm tôi mất cả đêm thứ Năm.
for (auto& param : network.parameters()) {
    param->grad = 0.0;  // Dòng này đã thiếu -_-
}

Khám phá quá tải toán tử

Việc triển khai operator+operator* cho lớp Value thật sự hài lòng. Bạn viết c = a + b và ở phía sau, bạn đang xây dựng một đồ thị tính toán nhớ cách để backpropagate các đạo hàm. Nó giống như đường dẫn cú pháp cho phân biệt tự động.

Quy tắc chuỗi trong hành động

Khi xem quá trình ngược lại hoạt động, đó là khoảnh khắc mọi thứ trở nên rõ ràng. Bạn gọi loss.backward() và nó lan tỏa ngược qua toàn bộ đồ thị tính toán, mỗi nút biết chính xác cách truyền đạo hàm cho các nút con của nó. Giống như xem những quân domino đổ ngược lại.

Những gì mà dự án này thực sự đạt được

Hãy thực tế về phạm vi ở đây. Triển khai C++ của tôi xử lý:

  • Các phép toán cơ bản (cộng, nhân, lũy thừa, tanh)
  • Mạng perceptron nhiều lớp
  • Lỗi bình phương trung bình
  • Giảm dần đạo hàm đơn giản

Chỉ vậy thôi. Không có bộ tối ưu, không có điều chỉnh, không có kiến trúc phức tạp. Nó gần như không thể giải quyết XOR, chứ đừng nói đến việc tạo thơ.

Nhưng đó chính xác là điểm. Bây giờ tôi hiểu, ở một mức độ sâu sắc, điều gì xảy ra khi PyTorch gọi loss.backward(). Tôi hiểu tại sao bạn cần phải đặt đạo hàm về 0. Tôi hiểu đồ thị tính toán thực sự là gì.

Kiểm tra với phiên bản gốc

Sự xác nhận thực sự là đảm bảo rằng phiên bản C++ của tôi tạo ra kết quả giống hệt như phiên bản Python của Karpathy. Cùng một hạt giống ngẫu nhiên, cùng dữ liệu, cùng hyperparameters – các đường cong lỗi nên giống nhau.

Chúng đã giống nhau. Và điều đó cảm thấy tốt hơn bất kỳ bài kiểm tra đơn vị nào.

cpp Copy
// Vòng lặp huấn luyện phản ánh micrograd chính xác
for (int epoch = 0; epoch < 100; ++epoch) {
    auto pred = network.forward(inputs);
    auto loss = mean_squared_error(pred, targets);

    zero_gradients(network.parameters());
    loss->backward();

    for (auto& p : network.parameters()) {
        p->data -= 0.05 * p->grad;  // Cùng tỷ lệ học như ban đầu
    }

    if (epoch % 10 == 0) {
        std::cout << "epoch " << epoch << " loss " << loss->data << std::endl;
    }
}

Giá trị thực sự của việc quay ngược lại

Dự án này không tiến bộ trong lĩnh vực công nghệ. Nó không giải quyết bất kỳ vấn đề mới nào. Nó thậm chí không triển khai bất cứ điều gì mới.

Nhưng nó nhắc tôi nhớ lý do tại sao tôi yêu lĩnh vực này. Có điều gì đó rất thỏa mãn khi hiểu công cụ của bạn ở mức độ thấp nhất. Khi tôi sử dụng PyTorch bây giờ, tôi không chỉ đang gọi những hàm ma thuật; tôi hiểu những hàm đó thực sự đang làm gì bên dưới.

Hơn nữa, kỹ năng C++ của tôi chắc chắn đã không còn cùn nữa.

Dành cho những ai đang xem xét các dự án tương tự

Nếu bạn đang nghĩ đến việc triển khai micrograd cho riêng mình (trong C++ hoặc ngôn ngữ khác), hãy làm đi. Mã nguồn đã có trên GitHub nếu bạn muốn xem cách tôi tiếp cận việc chuyển đổi, nhưng thật sự, giá trị nằm ở việc tự mình làm điều đó.

Cảnh báo công bằng: nó sẽ mất nhiều thời gian hơn bạn kỳ vọng. Không phải vì các khái niệm khó, mà vì việc quản lý tất cả các con trỏ và quá tải toán tử đúng cách mất thời gian. Và việc gỡ lỗi các lỗi tính toán đạo hàm là một loại địa ngục riêng.

Nhưng khi bạn cuối cùng thấy đường cong lỗi giảm, khi bạn nhận ra rằng động cơ phân biệt tự động tự tay viết của bạn thực sự đang dạy một mạng nơ-ron học... thì nó thật sự đáng giá.

Tiếp theo: dạy các mạng nơ-ron tôn trọng các định luật vật lý. Chắc chắn sẽ thú vị.


Đây là một phần của loạt bài "quay lại với cơ bản" nơi tôi tái xây dựng hiểu biết của mình từ đầu. Theo dõi tôi khi tôi làm việc qua PIML và cuối cùng đóng góp cho cộng đồng.


Triển khai đầy đủ: hadywalied/cGrad

Nguồn cảm hứng ban đầu: karpathy/micrograd


Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào