14 điểm bởi GN⁺ 2025-05-16 | 4 bình luận | Chia sẻ qua WhatsApp
  • Tác giả trình bày những vấn đề của NumPy kèm nhiều ví dụ minh họa
  • Các phép toán mảng đơn giản thì dễ với NumPy, nhưng khi số chiều tăng lên thì độ phức tạp và sự rối rắm cũng tăng vọt
  • Thiết kế của NumPy như broadcasting và advanced indexing còn thiếu sự rõ ràng và khả năng trừu tượng hóa
  • Việc viết mã buộc phải dựa vào phỏng đoán và thử-sai thay vì chỉ định trục một cách tường minh
  • Tác giả đưa ra ý tưởng về một ngôn ngữ mảng được cải tiến và sẽ giới thiệu các phương án thay thế cụ thể trong bài viết tiếp theo

Mở đầu: Yêu ghét lẫn lộn với NumPy

  • Tác giả cho biết đã dùng NumPy trong thời gian dài nhưng cũng rất thất vọng với các giới hạn của nó
  • NumPy là một thư viện thiết yếu và có ảnh hưởng lớn cho phép toán mảng trong Python
  • Các thư viện machine learning hiện đại như PyTorch cũng tồn tại những vấn đề tương tự NumPy

Điều dễ và điều khó ở NumPy

  • Những phép toán đơn giản như giải hệ phương trình tuyến tính cơ bản có thể thực hiện bằng cú pháp rõ ràng và thanh lịch
  • Nhưng khi số chiều của mảng tăng lên hoặc phép toán trở nên phức tạp hơn, việc xử lý hàng loạt mà không dùng for loop trở thành bắt buộc
  • Trong các môi trường không thể dùng vòng lặp trực tiếp (như tính toán trên GPU), cần đến cú pháp vector hóa đặc thù hoặc cách gọi hàm đặc biệt
  • Tuy nhiên, cách dùng chính xác của các hàm này lại mơ hồ và khó hiểu rõ chỉ qua tài liệu
  • Trên thực tế, với mảng nhiều chiều, gần như không ai có thể tự tin chắc chắn phải dùng linalg.solve của numpy như thế nào cho đúng

Các vấn đề của NumPy

  • NumPy thiếu một lý thuyết nhất quán để áp dụng phép toán lên một phần hay một trục cụ thể của mảng nhiều chiều
  • Khi số chiều của mảng là 2 trở xuống thì còn rõ ràng, nhưng từ 3 chiều trở lên thì việc xác định trục nào là đối tượng của phép toán trở nên mơ hồ với từng mảng
  • Nó buộc người dùng phải dùng những cách phức tạp như None, broadcasting, np.tensordot để canh khớp số chiều một cách tường minh
  • Những cách này dễ gây sai sót, làm giảm khả năng đọc mã và tăng nguy cơ lỗi

Vòng lặp và sự rõ ràng

  • Nếu thực sự cho phép dùng vòng lặp, người ta có thể viết mã ngắn gọn và rõ ràng hơn
  • Mã dùng vòng lặp có thể trông kém tinh tế hơn, nhưng lại có lợi thế lớn về mặt rõ ràng
  • Ngược lại, khi số chiều mảng thay đổi, người viết phải cân nhắc từng phép transpose hay thứ tự trục, khiến độ phức tạp tăng lên

np.einsum: hàm hiếm hoi thực sự tốt

  • np.einsum rất mạnh vì cung cấp một ngôn ngữ đặc thù miền linh hoạt cho phép đặt tên các trục
  • einsum giúp ý định của phép toán trở nên rõ ràng và khả năng khái quát hóa cũng rất tốt, nên có thể hiện thực tường minh các phép toán trục phức tạp
  • Nhưng kiểu hỗ trợ phép toán giống einsum chỉ giới hạn ở một số phép toán, ví dụ không thể dùng cho linalg.solve

Vấn đề của broadcasting

  • Broadcasting, thủ thuật cốt lõi của NumPy, là tính năng tự động canh khớp khi số chiều không khớp nhau
  • Trong các trường hợp đơn giản thì tiện lợi, nhưng trên thực tế nó khiến việc nắm rõ số chiều trở nên khó hơn và có nhiều tình huống dễ lỗi
  • Vì broadcasting là ngầm định, mỗi lần đọc mã người ta lại phải kiểm tra phép toán thực sự hoạt động như thế nào

Sự mơ hồ của indexing

  • Advanced indexing của NumPy khiến việc dự đoán shape của mảng rất khó và thiếu rõ ràng
  • Tùy theo tổ hợp indexing khác nhau mà shape của mảng kết quả thay đổi, nên nếu chưa từng trực tiếp xử lý thì rất khó đoán trước
  • Tài liệu giải thích các quy tắc indexing cũng dài và phức tạp, làm tốn nhiều thời gian để học
  • Ngay cả khi chỉ muốn dùng indexing đơn giản, trong một số phép toán cụ thể người dùng vẫn buộc phải dùng advanced indexing

Giới hạn trong thiết kế hàm của NumPy

  • Nhiều hàm của NumPy chỉ được tối ưu cho một số shape mảng nhất định
  • Với mảng nhiều chiều, phải dùng thêm tham số axes, tên hàm khác, hoặc các quy ước riêng, nhưng giữa các hàm lại thiếu tính nhất quán
  • Đây là cấu trúc đi ngược lại nguyên tắc lập trình cơ bản là trừu tượng hóa và tái sử dụng
  • Ngay cả khi đã viết được một hàm giải quyết một vấn đề cụ thể, muốn áp dụng lại cho các mảng và trục khác nhau thì thường phải viết lại hẳn mã khác

Ví dụ thực tế: triển khai self-attention

  • Khi viết self-attention bằng NumPy, nếu dùng vòng lặp thì rõ ràng hơn, nhưng nếu buộc phải vector hóa thì mã sẽ phức tạp
  • Khi cần các phép toán nhiều chiều như multi-head attention, phải kết hợp einsum với chuyển đổi trục, khiến mã trở nên khó hiểu

Kết luận và phương án thay thế

  • Tác giả cho biết NumPy là "lựa chọn duy nhất vừa có nhiều điểm tệ hơn các ngôn ngữ mảng khác, vừa trở nên quan trọng trên thị trường đến mức không thể bỏ qua"
  • Để vượt qua nhiều vấn đề của NumPy (broadcasting, sự mơ hồ của indexing, tính thiếu nhất quán của hàm, v.v.), tác giả cho biết đã tạo một nguyên mẫu ngôn ngữ mảng cải tiến
  • Các đề xuất cải tiến cụ thể (API ngôn ngữ mảng mới) sẽ được giới thiệu trong một bài viết riêng sau này

4 bình luận

 
youn17 2025-05-16

Nghe giống như câu chuyện về lý do Julia ra đời. Dù phải học các thư viện, nhưng vì nó giải quyết được nhiều vấn đề của NumPy nên có vẻ là một lựa chọn thật sự rất hấp dẫn.

 
ahwjdekf 2025-05-16

numpy nếu không tận dụng tốt vectorization thì hiệu năng sẽ tệ hẳn. Viết có tính đến mấy thứ đó vừa căng thẳng vừa khó.

 
domino 2025-05-16

Mấy thư viện Python hơi cũ có vẻ đều gặp những vấn đề khá giống nhau.

 
GN⁺ 2025-05-16
Ý kiến Hacker News
  • Ở ví dụ đầu tiên, nếu chỉ nhìn kiểu của b rồi đọc tài liệu thì sẽ khó hiểu, nhưng vì có mô tả về shape được trả về nên cần kiểm tra xem vector b thực sự có dạng ma trận hay không, đặc biệt là khi K=1
  • Nếu mảng có hơn 2 chiều thì nên dùng Xarray, thư viện thêm tên chiều cho mảng Numpy; nhờ vậy broadcasting/căn chỉnh được tự động mà không cần khớp chiều hay transpose, nên phần lớn các vấn đề kiểu này được giải quyết; Xarray yếu hơn NumPy về mặt đại số tuyến tính nhưng có thể dễ dàng quay lại NumPy và chỉ cần viết vài hàm trợ giúp; dùng Xarray giúp tăng năng suất đáng kể khi xử lý dữ liệu từ 3 chiều trở lên
    • Xarray giống như sự kết hợp ưu điểm của Pandas và NumPy, việc indexing như da.sel(x=some_x).isel(t=-1).mean(["y", "z"]) rất dễ, broadcasting cũng rõ ràng vì tên chiều được tôn trọng, và nó mạnh trong xử lý dữ liệu địa không gian với nhiều CRS khác nhau; kết hợp với Arviz cũng rất tốt nên việc xử lý thêm chiều trong phân tích Bayes trở nên dễ dàng; cũng có thể gộp nhiều mảng vào một dataset để chia sẻ cùng tọa độ, nên có thể dễ dàng áp dụng cho mọi mảng có trục thời gian như ds.isel(t=-1)
    • Nhờ Xarray mà nhu cầu dùng NumPy kiểu cơ bản giảm đi nhiều và năng suất tăng lên rõ rệt
    • Không biết trong các framework như Tensorflow, Keras, Pytorch có thứ tương tự không; nhớ là trước đây từng rất vất vả khi debug những vấn đề đã nhắc tới
    • Cảm ơn vì bài giới thiệu, chắc chắn sẽ thử; trước giờ cứ nghĩ chỉ mình thấy cú pháp như array[:, :, None] là khó chịu, nên rất vui khi thấy có người cùng quan điểm
    • Trong lĩnh vực biosignal, NeuroPype trên nền NumPy hỗ trợ trục có tên cho tensor n chiều và có thể lưu dữ liệu per-element theo từng trục như tên kênh, vị trí, v.v.
    • Điều này gợi nhớ thời NumPy tách ra từ các thư viện Numeric và Numarray; có thể tưởng tượng phe Numarray đã kiên trì tranh luận suốt 20 năm, rồi được rót vốn, đổi tên thành Xarray và cuối cùng đánh bại NumPy được chăng (tất nhiên phần lớn là hư cấu)
  • Một trong những lý do tôi bắt đầu dùng Julia là vì cú pháp NumPy quá khó; khi chuyển từ MATLAB sang NumPy, tôi thấy mình kém lập trình hơn và dành thời gian học các mẹo hiệu năng thay vì làm toán; với Julia thì cả vector hóa lẫn vòng lặp đều hoạt động tốt nên chỉ cần quan tâm đến độ dễ đọc của mã; tôi cảm nhận rất rõ những trải nghiệm và cảm xúc đó trong bài viết; tôi cũng không cho rằng cách tiếp cận “hộp đen” kiểu bắt mọi người phải dùng np.linalg.solve vì cho rằng đó luôn là nhanh nhất là đúng đắn; có nhiều lý do khiến việc tự viết kernel chuyên biệt cho bài toán lại tốt hơn
    • Nguyên nhân là Julia là ngôn ngữ được thiết kế cho tính toán khoa học, còn NumPy chỉ là thư viện bị ép đặt lên trên một ngôn ngữ vốn không dành cho tính toán khoa học; hy vọng một ngày nào đó Julia thắng thế để những người dùng Python chỉ vì hiệu ứng mạng lưới được giải thoát
    • MATLAB cũng chậm như Python nếu chạy vòng lặp mà không vector hóa; vấn đề lớn nhất là Python chậm, Julia rõ ràng có ưu điểm nhưng trên thực tế lại chỉ dùng được cho các mục đích khá hạn chế; Python đã có thêm mấy kiểu JIT hack nhưng vẫn chưa hoàn chỉnh; rất cần một phương án thay thế Python
    • MATLAB có thực sự khác không? Vòng lặp vẫn chậm như cũ, và thứ nhanh nhất vẫn là các “hộp đen” đã được tối ưu hoàn toàn như toán tử \\
    • Các phiên bản Fortran hiện đại cũng cho phép cả vector hóa lẫn vòng lặp chạy nhanh như Julia, nên có thể chỉ tập trung vào tính dễ đọc
  • Nếu tổng hợp các bất mãn với numpy so với Matlab, Julia thì đó là: mỗi hàm lại có tham số liên quan đến trục, cách đặt tên và kiểu hỗ trợ vector hóa khác nhau; và nếu muốn áp dụng hàm lên một trục nào đó thì thường phải viết lại mã hoàn toàn; trong khi nền tảng của lập trình là trừu tượng hóa thì NumPy lại làm điều đó khó hơn; ở Matlab, mã vector hóa thường gần như chạy nguyên trạng hoặc cách sửa rất rõ ràng, còn với NumPy thì lúc nào cũng phải lục tài liệu, việc khớp kiểu bằng transpose/reshape cũng không nhất quán nên rất mơ hồ
    • Hỗ trợ mảng từ 3 chiều trở lên của Matlab quá yếu nên ngược lại những vấn đề được nêu trong bài lại ít khi xảy ra
    • Với vấn đề thứ hai thì có thể thử jax no vmap
    • Việc viết một hàm cho mảng 2x2 rồi muốn áp dụng lên một phần của mảng 3x2x2 thì có thể làm bằng slice và squeeze các kiểu; bản thân vấn đề này mơ hồ đến mức khó mà hiểu nó là vấn đề gì
    • Có thể xử lý bằng reshape
  • Điều gây rối nhất trong numpy là không rõ phép toán nào sẽ chạy theo kiểu vector hóa, và cũng không thể biểu đạt tường minh bằng cú pháp dot như Julia; cả về kiểu trả về cũng có rất nhiều bẫy; ví dụ với đối tượng poly1d P, nếu nhân từ bên phải với z0 thì kết quả vẫn là poly1d, nhưng nếu nhân từ bên trái theo dạng z0*P thì chỉ trả về mảng, nên kiểu bị chuyển đổi một cách âm thầm; ngay cả hệ số đầu của đa thức bậc hai cũng có thể truy cập bằng cả P.coef[0]P[2], rất dễ gây nhầm lẫn; chính thức thì poly1d là API “cũ” và mã mới được khuyến nghị dùng lớp Polynomial, nhưng trên thực tế cũng không có cảnh báo deprecated nào; những chỗ như vậy, nơi có chuyển kiểu và dữ liệu không nhất quán, nằm rải rác khắp thư viện như mìn gài và biến việc debug thành ác mộng
  • Tôi đồng cảm với những gì tác giả chỉ ra; khi chuyển từ Matlab sang Numpy có rất nhiều bất tiện, và tôi cũng thấy việc slice dữ liệu trong Numpy bất tiện hơn Matlab/Julia; nhưng nếu tính đến chi phí license toolbox của Matlab thì các nhược điểm của Numpy vẫn có thể chấp nhận được; các vấn đề trong bài chủ yếu xuất hiện với tensor hơn 2 chiều, mà Numpy vốn dựa trên ma trận (2D) nên giới hạn đó cũng dễ hiểu; thư viện chuyên dụng như Torch có thể tốt hơn nhưng cũng không hề dễ; rốt cuộc cảm giác đúng là: "NumPy hơi tệ hơn một chút so với các ngôn ngữ mảng khác, nhưng lại chẳng có nhiều thứ khác để dùng"
    • Ngay từ đầu Numpy đã nhắm tới mảng N chiều như phần nối tiếp của numarray, nên không phải chỉ dừng ở 2D
  • Vấn đề lớn nhất của hệ sinh thái data science Python là mọi thứ đều phi chuẩn; hơn chục thư viện hoạt động khác nhau chẳng khác gì 4 ngôn ngữ riêng biệt, và thứ duy nhất tương đối thống nhất chỉ là to_numpy(); cuối cùng thời gian đổi qua đổi lại định dạng dữ liệu còn nhiều hơn thời gian giải quyết bài toán; Julia cũng không phải chỉ có ưu điểm, nhưng khả năng liên thông giữa các thư viện như đơn vị đo và độ bất định là rất tốt, còn Python thì lúc nào cũng cần rất nhiều mã boilerplate
    • Dự án array-api đang cố gắng chuẩn hóa API thao tác mảng trên toàn bộ hệ sinh thái Python
    • R thậm chí còn phức tạp hơn vì có tới 4 hệ thống class
  • Tôi thắc mắc tại sao mọi người lại dùng numpy thay vì sage
  • Một số vấn đề có thể được giải quyết bằng numpysanegnuplotlib; từ khi có bộ đôi này thì tôi tích cực dùng numpy cho mọi việc; nếu không có chúng thì gần như không thể chịu nổi
    • numpysane rốt cuộc vẫn là vòng lặp Python, không phải vector hóa thực sự
    • Cảm ơn vì đã giới thiệu; tôi cũng hay càm ràm về các vấn đề này nhưng chưa từng nghĩ là đã có một thư viện bậc cao đơn giản như vậy
  • Để làm vectorized multi-head attention, tôi đã đưa mọi phép nhân ma trận vào einsum và dùng optimize="optimal" để áp dụng thuật toán nhân chuỗi ma trận nhằm tăng hiệu năng; thực tế đúng là nhanh hơn khoảng 2 lần so với cách vector hóa thông thường, nhưng điều đáng ngạc nhiên là bản cài đặt ngây thơ dùng vòng lặp lại còn nhanh hơn; ai tò mò về lý do thì hãy xem mã nguồn; tôi đoán vẫn còn chỗ để cải thiện cache coherency bên trong einsum