Newaxis là gì

Tôi đang sử dụng NumPy để xử lý một số ma trận dữ liệu lớn [có kích thước khoảng ~ 50GB]. Máy mà tôi đang chạy mã này có RAM 128GB nên việc thực hiện các hoạt động tuyến tính đơn giản ở mức độ này không phải là vấn đề về bộ nhớ.Tuy nhiên, tôi đang chứng kiến ​​sự tăng trưởng bộ nhớ rất lớn [lên hơn 100GB] khi tính toán đoạn mã sau bằng Python:import numpy as np # memory allocations [everything works fine] a = np.zeros[[1192953, 192, 32], dtype='f8'] b = np.zeros[[1192953, 192], dtype='f8'] c = np.zeros[[192, 32], dtype='f8'] a[:] = b[:, :, np.newaxis] - c[np.newaxis, :, :] # memory explodes here Xin lưu ý rằng việc cấp phát bộ nhớ ban đầu được thực hiện mà không gặp bất kỳ sự cố nào. Tuy nhiên, khi tôi cố gắng thực hiện thao tác trừ với phát sóng, bộ nhớ tăng lên hơn 100GB. Tôi luôn nghĩ rằng việc phát sóng sẽ tránh được việc phân bổ thêm bộ nhớ nhưng bây giờ tôi không chắc liệu điều này có luôn như vậy hay không.Như vậy, ai đó có thể cung cấp một số chi tiết về lý do tại sao sự tăng trưởng bộ nhớ này đang diễn ra và làm thế nào mã sau đây có thể được viết lại bằng cách sử dụng các cấu trúc hiệu quả hơn về bộ nhớ?

Tôi đang chạy mã bằng Python 2.7 trong IPython Notebook. Đề xuất của

Câu trả lời

@ rth để thực hiện hoạt động trong các lô nhỏ hơn là một gợi ý tốt. Bạn cũng có thể thử sử dụng hàm np.subtract và cung cấp cho nó mảng đích để tránh tạo mảng tạm thời bổ sung. Tôi cũng nghĩ rằng bạn không cần phải lập chỉ mục c là c[np.newaxis, :, :], bởi vì nó đã là một mảng 3-d.Vì vậy, thay vìa[:] = b[:, :, np.newaxis] - c[np.newaxis, :, :] # memory explodes here cố gắngnp.subtract[b[:, :, np.newaxis], c, a]
Đối số thứ ba của np.subtract là mảng đích.

  • Bắt đầu hình dạng: Giới thiệu về Mảng NumPy
  • Vectorization là gì?
    • Đếm: Dễ dàng như 1, 2, 3…
    • Mua thấp, bán cao
  • Intermezzo: Hiểu ký hiệu Axes
  • Phát thanh truyền hình
  • Lập trình mảng trong hành động: Ví dụ
    • Thuật toán phân cụm
    • Bảng phân bổ
    • Trích xuất tính năng hình ảnh
  • Suy nghĩ chia tay: Đừng tối ưu hóa quá mức
  • Nhiêu tai nguyên hơn

Đôi khi người ta nói rằng Python, so với các ngôn ngữ cấp thấp như C ++ , cải thiện thời gian phát triển với chi phí là thời gian chạy. May mắn thay, có một số cách để tăng tốc thời gian chạy hoạt động trong Python mà không làm mất đi tính dễ sử dụng. Một tùy chọn phù hợp cho các hoạt động số nhanh là NumPy, ứng dụng này xứng đáng được coi là gói cơ bản cho tính toán khoa học với Python.

Đúng là, ít người sẽ phân loại thứ gì đó mất 50 micro giây [năm mươi phần triệu giây] là “chậm”. Tuy nhiên, các máy tính có thể khác nhau. Thời gian chạy của một hoạt động tham gia 50 micro giây [50 ms] thuộc lĩnh vực microperformance , mà lỏng lẻo có thể được định nghĩa là các hoạt động với một thời gian chạy giữa 1 micro giây và 1 mili giây.

Tại sao tốc độ lại quan trọng? Lý do mà hiệu suất vi mô đáng được theo dõi là do sự khác biệt nhỏ trong thời gian chạy trở nên khuếch đại với các lệnh gọi hàm lặp lại: chi phí tăng thêm 50 μs, lặp lại hơn 1 triệu lệnh gọi hàm, chuyển thành 50 giây thời gian chạy tăng dần.

Khi nói đến tính toán, thực sự có ba khái niệm cho thấy sức mạnh của NumPy:

  • Vectơ hóa
  • Phát thanh truyền hình
  • Lập chỉ mục

Trong hướng dẫn này, bạn sẽ thấy từng bước cách tận dụng vectơ hóa và phát sóng , để bạn có thể sử dụng NumPy hết công suất của nó. Trong khi bạn sẽ sử dụng một số lập chỉ mục trong thực tế ở đây, các sơ đồ lập chỉ mục hoàn chỉnh của NumPy, mở rộng cú pháp cắt của Python , là con thú của riêng họ. Nếu bạn đang muốn đọc thêm về lập chỉ mục NumPy, hãy uống một chút cà phê và đi đến phần Lập chỉ mục trong tài liệu NumPy.

Bắt đầu hình dạng: Giới thiệu về Mảng NumPy

Đối tượng cơ bản của NumPy là ndarray[hoặc numpy.array] của nó , một mảng n chiều cũng có mặt ở một số dạng trong các ngôn ngữ hướng mảng như Fortran 90, R và MATLAB , cũng như các ngôn ngữ tiền nhiệm APL và J.

Hãy bắt đầu mọi thứ bằng cách tạo một mảng 3 chiều với 36 phần tử:

>>> >>> import numpy as np >>> arr = np.arange[36].reshape[3, 4, 3] >>> arr array[[[[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]], [[12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23]], [[24, 25, 26], [27, 28, 29], [30, 31, 32], [33, 34, 35]]]]

Việc vẽ các mảng chiều cao theo hai chiều có thể khó khăn. Một cách trực quan để nghĩ về hình dạng của một mảng là chỉ cần “đọc nó từ trái sang phải.” arrlà mảng 3 x 4 x 3 :

>>> >>> arr.shape [3, 4, 3]

Trực quan, arrcó thể được coi như một thùng chứa ba lưới 4x3 [hoặc một lăng trụ hình chữ nhật] và sẽ giống như sau:

Các mảng có chiều cao hơn có thể khó hình dung hơn, nhưng chúng vẫn sẽ tuân theo mô hình “mảng trong một mảng” này.

Bạn có thể thấy dữ liệu có nhiều hơn hai thứ nguyên ở đâu?

  • Dữ liệu bảng điều khiển có thể được biểu diễn theo ba chiều. Dữ liệu theo dõi các thuộc tính của một nhóm [nhóm] cá nhân theo thời gian có thể được cấu trúc như[respondents, dates, attributes]. Cuộc khảo sát dọc quốc gia về thanh niên năm 1979theo dõi 12.686 người trả lời trong 27 năm. Giả sử rằng bạn có ~ 500 điểm dữ liệu được hỏi trực tiếp hoặc lấy từ mỗi cá nhân, mỗi năm, dữ liệu này sẽ có hình dạng[12686, 27, 500]với tổng số 177.604.000 điểm dữ liệu.
  • Dữ liệu hình ảnh màu cho nhiều hình ảnh thường được lưu trữ trong bốn chiều. Mỗi hình ảnh là một mảng ba chiều [height, width, channels], trong đó các kênh thường là các giá trị đỏ, lục và lam [RGB]. Sau đó, một bộ sưu tập các hình ảnh chỉ là [image_number, height, width, channels]. Một nghìn hình ảnh 256x256 RGB sẽ có hình dạng [1000, 256, 256, 3]. [Một đại diện mở rộng là RGBA, trong đó A – alpha – biểu thị mức độ mờ.]

Để biết thêm chi tiết về các ví dụ trong thế giới thực về dữ liệu chiều cao, hãy xem Chương 2 của Học sâu với Python của François Chollet .

Vectorization là gì?

Vectơ hóa là một khả năng mạnh mẽ trong NumPy để thể hiện các hoạt động xảy ra trên toàn bộ mảng thay vì các phần tử riêng lẻ của chúng. Đây là một định nghĩa ngắn gọn từ Wes McKinney:

Thực hành thay thế các vòng lặp rõ ràng bằng các biểu thức mảng thường được gọi là vectơ hóa. Nói chung, các phép toán mảng được vectơ hóa thường sẽ nhanh hơn một hoặc hai [hoặc nhiều] bậc lớn hơn so với các phép toán tương đương trong Python thuần túy của chúng, với tác động lớn nhất [đã thấy] trong bất kỳ loại tính toán số nào. [ nguồn ]

Khi lặp qua một mảng hoặc bất kỳ cấu trúc dữ liệu nào trong Python, có rất nhiều chi phí liên quan. Các hoạt động được vector hóa trong NumPy ủy quyền vòng lặp nội bộ cho các hàm C và Fortran được tối ưu hóa cao , giúp tạo mã Python nhanh hơn và sạch hơn.

Đếm: Dễ dàng như 1, 2, 3…

Như một minh hoạ, hãy xem xét một vector 1 chiều Truevà Falsemà bạn muốn đếm số lượng “False True” hiệu ứng chuyển tiếp trong trình tự:

>>> >>> np.random.seed[444] >>> x = np.random.choice[[False, True], size=100000] >>> x array[[ True, False, True, ..., True, False, True]]

Với vòng lặp for Python , một cách để thực hiện điều này là đánh giá theo cặp giá trị chân lý của mỗi phần tử trong chuỗi cùng với phần tử đứng ngay sau nó:

>>> >>> def count_transitions[x] -> int: ... count = 0 ... for i, j in zip[x[:-1], x[1:]]: ... if j and not i: ... count += 1 ... return count ... >>> count_transitions[x] 24984

Ở dạng vectơ, không có vòng lặp for rõ ràng hoặc tham chiếu trực tiếp đến các phần tử riêng lẻ:

>>> >>> np.count_nonzero[x[:-1] >> >>> from timeit import timeit >>> setup = 'from __main__ import count_transitions, x; import numpy as np' >>> num = 1000 >>> t1 = timeit['count_transitions[x]', setup=setup, number=num] >>> t2 = timeit['np.count_nonzero[x[:-1] < x[1:]]', setup=setup, number=num] >>> print['Speed difference: {:0.1f}x'.format[t1 / t2]] Speed difference: 71.0x

Chi tiết kỹ thuật : Một thuật ngữ khác là bộ xử lý vectơ , có liên quan đến phần cứng của máy tính. Khi tôi nói về vectơ hóa ở đây, tôi đang đề cập đến khái niệm thay thế các vòng lặp for rõ ràng bằng các biểu thức mảng, trong trường hợp này có thể tính toán nội bộ bằng một ngôn ngữ cấp thấp.

Mua thấp, bán cao

Đây là một ví dụ khác để kích thích sự thèm ăn của bạn. Hãy xem xét vấn đề phỏng vấn kỹ thuật cổ điển sau :

Với lịch sử giá của một cổ phiếu là một chuỗi và giả sử rằng bạn chỉ được phép thực hiện một lần mua và một lần bán, thì lợi nhuận tối đa có thể thu được là bao nhiêu? Ví dụ: đã cho prices = [20, 18, 14, 17, 20, 21, 15], lợi nhuận tối đa sẽ là 7, từ việc mua ở mức 14 và bán ở mức 21.

[Đối với tất cả các bạn tài trợ cho những người: không, không được phép bán khống.]

Có một giải pháp với độ phức tạp thời gian bình phương n bao gồm việc lấy mọi kết hợp của hai mức giá trong đó giá thứ hai “đứng sau” giá thứ nhất và xác định mức chênh lệch tối đa.

Tuy nhiên, cũng có một giải pháp O [n] bao gồm việc lặp lại chuỗi chỉ một lần và tìm ra sự khác biệt giữa mỗi mức giá và mức tối thiểu đang chạy . Nó đi một cái gì đó như thế này:

>>> >>> def profit[prices]: ... max_px = 0 ... min_px = prices[0] ... for px in prices[1:]: ... min_px = min[min_px, px] ... max_px = max[px - min_px, max_px] ... return max_px >>> prices = [20, 18, 14, 17, 20, 21, 15] >>> profit[prices] 7

Điều này có thể được thực hiện trong NumPy? Bạn đặt cược. Nhưng trước tiên, hãy xây dựng một ví dụ gần như thực tế:

>>> # Create mostly NaN array with a few 'turning points' [local min/max]. >>> prices = np.full[100, fill_value=np.nan] >>> prices[[0, 25, 60, -1]] = [80., 30., 75., 50.] # Linearly interpolate the missing values and add some noise. >>> x = np.arange[len[prices]] >>> is_valid = ~np.isnan[prices] >>> prices = np.interp[x=x, xp=x[is_valid], fp=prices[is_valid]] >>> prices += np.random.randn[len[prices]] * 2

Đây là những gì điều này trông như thế nào với matplotlib . Câu ngạn ngữ là mua thấp [xanh] và bán cao [đỏ]:

>>> >>> import matplotlib.pyplot as plt # Warning! This isn't a fully correct solution, but it works for now. # If the absolute min came after the absolute max, you'd have trouble. >>> mn = np.argmin[prices] >>> mx = mn + np.argmax[prices[mn:]] >>> kwargs = {'markersize': 12, 'linestyle': ''} >>> fig, ax = plt.subplots[] >>> ax.plot[prices] >>> ax.set_title['Price History'] >>> ax.set_xlabel['Time'] >>> ax.set_ylabel['Price'] >>> ax.plot[mn, prices[mn], color='green', **kwargs] >>> ax.plot[mx, prices[mx], color='red', **kwargs]

Việc triển khai NumPy trông như thế nào? Mặc dù không có np.cummin[]"trực tiếp", tất cả các hàm phổ quát của NumPy [ufuncs] đều có một accumulate[]phương thức thực hiện những gì tên của nó ngụ ý:

>>> >>> cummin = np.minimum.accumulate

Mở rộng logic từ ví dụ thuần Python, bạn có thể tìm thấy sự khác biệt giữa từng mức giá và mức tối thiểu đang chạy [theo phần tử] , sau đó lấy giá trị tối đa của chuỗi này:

>>> >>> def profit_with_numpy[prices]: ... """Price minus cumulative minimum price, element-wise.""" ... prices = np.asarray[prices] ... return np.max[prices - cummin[prices]] >>> profit_with_numpy[prices] 44.2487532293278 >>> np.allclose[profit_with_numpy[prices], profit[prices]] True

Làm thế nào để hai hoạt động này, có cùng độ phức tạp về thời gian lý thuyết , so sánh trong thời gian chạy thực tế? Đầu tiên, hãy xem một trình tự dài hơn. [Đây không nhất thiết phải là một chuỗi thời gian của giá cổ phiếu tại thời điểm này.]

>>> >>> seq = np.random.randint[0, 100, size=100000] >>> seq array[[ 3, 23, 8, 67, 52, 12, 54, 72, 41, 10, ..., 46, 8, 90, 95, 93, 28, 24, 88, 24, 49]]

Bây giờ, để so sánh có phần không công bằng:

>>> >>> setup = ['from __main__ import profit_with_numpy, profit, seq;' ... ' import numpy as np'] >>> num = 250 >>> pytime = timeit['profit[seq]', setup=setup, number=num] >>> nptime = timeit['profit_with_numpy[seq]', setup=setup, number=num] >>> print['Speed difference: {:0.1f}x'.format[pytime / nptime]] Speed difference: 76.0x

Ở trên, được coi profit_with_numpy[]là mã giả [không xem xét cơ chế cơ bản của NumPy], thực tế có ba lần đi qua một chuỗi:

  • cummin[prices] có độ phức tạp thời gian O [n]
  • prices - cummin[prices] là O [n]
  • max[...] là O [n]

Điều này giảm xuống còn O [n], bởi vì O [3n] giảm chỉ còn O [n] – n “chiếm ưu thế” khi n tiến đến vô cùng.

Do đó, hai hàm này có độ phức tạp thời gian trong trường hợp xấu nhất tương đương nhau . [Mặc dù, như một lưu ý phụ, hàm NumPy đi kèm với sự phức tạp về không gian hơn đáng kể.] Nhưng đó có lẽ là điều ít quan trọng nhất ở đây. Một bài học là, trong khi độ phức tạp về thời gian lý thuyết là một yếu tố quan trọng cần cân nhắc, thì cơ học thời gian chạy cũng có thể đóng một vai trò lớn. NumPy không chỉ có thể ủy quyền cho C, mà với một số phép toán thông minh phần tử và đại số tuyến tính, nó cũng có thể tận dụng lợi thế của tính toán trong nhiều luồng. Nhưng có rất nhiều yếu tố đang diễn ra ở đây, bao gồm cả thư viện cơ bản được sử dụng [BLAS / LAPACK / Atlas], và những chi tiết đó hoàn toàn dành cho toàn bộ một bài báo.

Intermezzo: Hiểu ký hiệu Axes

Trong NumPy, một trục đề cập đến một chiều duy nhất của mảng đa chiều:

>>> >>> arr = np.array[[[1, 2, 3], ... [10, 20, 30]]] >>> arr.sum[axis=0] array[[11, 22, 33]] >>> arr.sum[axis=1] array[[ 6, 60]]

Thuật ngữ xung quanh các trục và cách mô tả chúng có thể hơi không trực quan. Trong tài liệu dành cho Pandas [một thư viện được xây dựng trên NumPy], bạn có thể thường xuyên thấy những thứ như:

axis : {'index' [0], 'columns' [1]}

Bạn có thể lập luận rằng, dựa trên mô tả này, các kết quả ở trên nên được "đảo ngược". Tuy nhiên, điều quan trọng là nó axisđề cập đến trục mà một hàm được gọi dọc theo . Điều này đã được Jake VanderPlas giải thích rõ ràng:

Cách trục được chỉ định ở đây có thể gây nhầm lẫn cho người dùng đến từ các ngôn ngữ khác. Từ khóa axis chỉ định kích thước của mảng sẽ được thu gọn, thay vì thứ nguyên sẽ được trả về. Vì vậy, chỉ định axis=0có nghĩa là trục đầu tiên sẽ được thu gọn: đối với mảng hai chiều, điều này có nghĩa là các giá trị trong mỗi cột sẽ được tổng hợp. [ nguồn ]

Nói cách khác, tính tổng một mảng để thu axis=0gọn các hàng của mảng bằng một phép tính theo cột .

Với sự khác biệt này, chúng ta hãy chuyển sang khám phá khái niệm phát sóng.

Phát thanh truyền hình

Phát sóng là một sự trừu tượng hóa NumPy quan trọng khác. Bạn đã thấy rằng các phép toán giữa hai mảng NumPy [có kích thước bằng nhau] hoạt động theo phần tử :

>>> >>> a = np.array[[1.5, 2.5, 3.5]] >>> b = np.array[[10., 5., 1.]] >>> a / b array[[0.15, 0.5 , 3.5 ]]

Tuy nhiên, đối với các mảng có kích thước không bằng nhau thì sao? Đây là nơi phát sóng:

Thuật ngữ phát sóng mô tả cách NumPy xử lý các mảng có hình dạng khác nhau trong các phép toán số học. Theo một số ràng buộc nhất định, mảng nhỏ hơn được "phát sóng" trên mảng lớn hơn để chúng có hình dạng tương thích. Broadcasting cung cấp một phương tiện vectơ hóa các hoạt động mảng để lặp lại xảy ra trong C thay vì Python. [ nguồn ]

Cách thực hiện phát sóng có thể trở nên tẻ nhạt khi làm việc với nhiều hơn hai mảng. Tuy nhiên, nếu chỉ có hai mảng, thì khả năng được phát sóng của chúng có thể được mô tả bằng hai quy tắc ngắn gọn:

Khi hoạt động trên hai mảng, NumPy sẽ so sánh các hình dạng của chúng theo phần tử. Nó bắt đầu với các kích thước theo sau và hoạt động theo cách của nó về phía trước. Hai thứ nguyên tương thích khi:

  1. họ bằng nhau, hoặc
  2. một trong số họ là 1

Thats tất cả để có nó.

Hãy xem trường hợp chúng ta muốn trừ mỗi giá trị trung bình theo cột của một mảng, theo phần tử:

>>> >>> sample = np.random.normal[loc=[2., 20.], scale=[1., 3.5], ... size=[3, 2]] >>> sample array[[[ 1.816 , 23.703 ], [ 2.8395, 12.2607], [ 3.5901, 24.2115]]]

Trong thuật ngữ thống kê, samplebao gồm hai mẫu [cột] được vẽ độc lập từ hai quần thể với giá trị tương ứng là 2 và 20. Giá trị trung bình theo cột phải gần đúng với trung bình tổng thể [mặc dù gần đúng, vì mẫu nhỏ]:

>>> >>> mu = sample.mean[axis=0] >>> mu array[[ 2.7486, 20.0584]]

Bây giờ, việc trừ đi các phương tiện theo cột rất đơn giản vì các quy tắc phát sóng kiểm tra:

>>> >>> print['sample:', sample.shape, '| means:', mu.shape] sample: [3, 2] | means: [2,] >>> sample - mu array[[[-0.9325, 3.6446], [ 0.091 , -7.7977], [ 0.8416, 4.1531]]]

Dưới đây là minh họa về việc trừ đi các phương tiện theo cột, trong đó một mảng nhỏ hơn được "kéo dài" để nó bị trừ khỏi mỗi hàng của mảng lớn hơn:

Chi tiết kỹ thuật : Mảng hoặc đại lượng vô hướng có kích thước nhỏ hơn không được kéo dài theo nghĩa đen trong bộ nhớ: chính quá trình tính toán được lặp lại.

Điều này cũng mở rộng đến việc chuẩn hóa từng cột, làm cho mỗi ô trở thành điểm số z so với cột tương ứng của nó:

>>> >>> [sample - sample.mean[axis=0]] / sample.std[axis=0] array[[[-1.2825, 0.6605], [ 0.1251, -1.4132], [ 1.1574, 0.7527]]]

Tuy nhiên, nếu bạn muốn trừ đi, vì một lý do nào đó, các giá trị tối thiểu theo hàng? Bạn sẽ gặp một chút rắc rối:

>>> >>> sample - sample.min[axis=1] ValueError: operands could not be broadcast together with shapes [3,2] [3,]

Vấn đề ở đây là mảng nhỏ hơn, ở dạng hiện tại, không thể "kéo dài" để tương thích với hình dạng sample. Bạn thực sự cần phải mở rộng kích thước của nó để đáp ứng các quy tắc phát sóng ở trên:

>>> >>> sample.min[axis=1][:, None] # 3 minimums across 3 rows array[[[1.816 ], [2.8395], [3.5901]]] >>> sample - sample.min[axis=1][:, None] array[[[ 0. , 21.887 ], [ 0. , 9.4212], [ 0. , 20.6214]]]

Lưu ý : [:, None]là một phương tiện để mở rộng số chiều của một mảng, để tạo ra một trục có độ dài bằng một. np.newaxislà một bí danh cho None.

Có một số trường hợp phức tạp hơn đáng kể. Dưới đây là một định nghĩa chặt chẽ hơn về thời điểm bất kỳ số lượng mảng tùy ý nào của bất kỳ hình dạng NumPy nào có thể được phát cùng nhau:

Một tập hợp các mảng được gọi là “có thể phát sóng” với cùng một hình dạng NumPy nếu các quy tắc sau tạo ra kết quả hợp lệ, có nghĩa là một trong những điều sau là đúng :

  1. Các mảng đều có hình dạng hoàn toàn giống nhau.

  2. Tất cả các mảng đều có cùng số kích thước và độ dài của mỗi thứ nguyên là độ dài chung hoặc 1.

  3. Các mảng có quá ít kích thước có thể có thêm hình dạng NumPy của chúng với kích thước có độ dài 1 để thỏa mãn thuộc tính # 2.

[ nguồn ]

Điều này dễ dàng hơn để thực hiện từng bước. Giả sử bạn có bốn mảng sau:

>>> >>> a = np.sin[np.arange[10][:, None]] >>> b = np.random.randn[1, 10] >>> c = np.full_like[a, 10] >>> d = 8

Trước khi kiểm tra các hình dạng, NumPy trước tiên chuyển đổi vô hướng thành mảng với một phần tử:

>>> >>> arrays = [np.atleast_1d[arr] for arr in [a, b, c, d]] >>> for arr in arrays: ... print[arr.shape] ... [10, 1] [1, 10] [10, 1] [1,]

Bây giờ chúng ta có thể kiểm tra tiêu chí số 1. Nếu tất cả các mảng có cùng hình dạng, một sethình dạng của chúng sẽ ngưng tụ thành một phần tử, bởi vì hàm set[]tạo sẽ loại bỏ các mục trùng lặp khỏi đầu vào của nó một cách hiệu quả. Tiêu chí này rõ ràng không được đáp ứng:

>>> >>> len[set[arr.shape for arr in arrays]] == 1 False

Phần đầu tiên của tiêu chí số 2 cũng không thành công, có nghĩa là toàn bộ tiêu chí không thành công:

>>> >>> len[set[[arr.ndim] for arr in arrays]] == 1 False

Tiêu chí cuối cùng liên quan nhiều hơn một chút:

Các mảng có quá ít kích thước có thể có thêm hình dạng của chúng với kích thước chiều dài 1 để thỏa mãn thuộc tính # 2.

Để mã hóa điều này, trước tiên bạn có thể xác định kích thước của mảng có chiều cao nhất và sau đó thêm các thứ nguyên vào từng shapebộ NumPy cho đến khi tất cả đều có kích thước bằng nhau:

>>> >>> maxdim = max[arr.ndim for arr in arrays] # Maximum dimensionality >>> shapes = np.array[[[1,] * [maxdim - arr.ndim] + arr.shape ... for arr in arrays]] >>> shapes array[[[10, 1], [ 1, 10], [10, 1], [ 1, 1]]]

Cuối cùng, bạn cần kiểm tra xem chiều dài của mỗi kích thước có phải là [được rút ra từ] chiều dài chung hay là 1 . Một mẹo để thực hiện điều này là trước tiên hãy che mảng NumPy “shape-tuples” ở những nơi mà nó bằng một. Sau đó, bạn có thể kiểm tra xem np.ptp[]chênh lệch giữa các cột từ đỉnh đến đỉnh [ ] có bằng 0 hay không:

>>> >>> masked = np.ma.masked_where[shapes == 1, shapes] >>> np.all[masked.ptp[axis=0] == 0] # ptp: max - min True

Được gói gọn trong một hàm duy nhất, logic này trông như thế này:

>>> >>> def can_broadcast[*arrays] -> bool: ... arrays = [np.atleast_1d[arr] for arr in arrays] ... if len[set[arr.shape for arr in arrays]] == 1: ... return True ... if len[set[[arr.ndim] for arr in arrays]] == 1: ... return True ... maxdim = max[arr.ndim for arr in arrays] ... shapes = np.array[[[1,] * [maxdim - arr.ndim] + arr.shape ... for arr in arrays]] ... masked = np.ma.masked_where[shapes == 1, shapes] ... return np.all[masked.ptp[axis=0] == 0] ... >>> can_broadcast[a, b, c, d] True

May mắn thay, bạn có thể sử dụng một phím tắt và sử dụng np.broadcast[]để kiểm tra sự tỉnh táo này, mặc dù nó không được thiết kế rõ ràng cho mục đích này:

>>> >>> def can_broadcast[*arrays] -> bool: ... try: ... np.broadcast[*arrays] ... return True ... except ValueError: ... return False ... >>> can_broadcast[a, b, c, d] True

Đối với những người quan tâm đến việc đào sâu hơn một chút, PyArray_Broadcastlà hàm C cơ bản đóng gói các quy tắc phát sóng.

Lập trình mảng trong hành động: Ví dụ

Trong 3 ví dụ sau, bạn sẽ đặt vectơ hóa và phát sóng để hoạt động với một số ứng dụng trong thế giới thực.

Thuật toán phân cụm

Máy học là một trong những lĩnh vực thường xuyên có thể tận dụng lợi thế của vectơ hóa và phát sóng. Giả sử rằng bạn có các đỉnh của một tam giác [mỗi hàng là một tọa độ x, y ]:

>>> >>> tri = np.array[[[1, 1], ... [3, 1], ... [2, 3]]]

Các trọng tâm của việc này “cụm” là một [x, y] phối hợp đó là trung bình cộng của mỗi cột:

>>> >>> centroid = tri.mean[axis=0] >>> centroid array[[2. , 1.6667]]

Thật hữu ích khi hình dung điều này:

>>> >>> trishape = plt.Polygon[tri, edgecolor='r', alpha=0.2, lw=5] >>> _, ax = plt.subplots[figsize=[4, 4]] >>> ax.add_patch[trishape] >>> ax.set_ylim[[.5, 3.5]] >>> ax.set_xlim[[.5, 3.5]] >>> ax.scatter[*centroid, color='g', marker='D', s=70] >>> ax.scatter[*tri.T, color='b', s=70]

Nhiều thuật toán phân cụm sử dụng khoảng cách Euclid của một tập hợp các điểm, đến điểm gốc hoặc liên quan đến tâm của chúng.

Trong hệ tọa độ Descartes, khoảng cách Euclide giữa điểm p và q là:

[ nguồn: Wikipedia ]

Vì vậy, đối với tập hợp các tọa độ tritừ bên trên, khoảng cách Euclide của mỗi điểm từ điểm gốc [0, 0] sẽ là:

>>> >>> np.sum[tri**2, axis=1] ** 0.5 # Or: np.sqrt[np.sum[np.square[tri], 1]] array[[1.4142, 3.1623, 3.6056]]

Bạn có thể nhận ra rằng chúng tôi thực sự chỉ đang tìm kiếm các chuẩn mực Euclide:

>>> >>> np.linalg.norm[tri, axis=1] array[[1.4142, 3.1623, 3.6056]]

Thay vì tham chiếu điểm gốc, bạn cũng có thể tìm chuẩn của mỗi điểm so với trọng tâm của tam giác:

>>> >>> np.linalg.norm[tri - centroid, axis=1] array[[1.2019, 1.2019, 1.3333]]

Cuối cùng, hãy tiến thêm một bước này: giả sử bạn có mảng 2d Xvà mảng 2d gồm nhiều [x, y] centroid “được đề xuất”. Các thuật toán như phân cụm K-Means hoạt động bằng cách chỉ định ngẫu nhiên các trung tâm “được đề xuất” ban đầu, sau đó gán lại mỗi điểm dữ liệu cho trung tâm gần nhất của nó. Từ đó, các trung tâm mới được tính toán, với thuật toán hội tụ vào một giải pháp khi các nhãn được tạo lại [mã hóa của các trung tâm] không thay đổi giữa các lần lặp. Một phần của quá trình lặp đi lặp lại này yêu cầu tính toán khoảng cách Euclide của mỗi điểm từ mỗi centroid :

>>> >>> X = np.repeat[[[5, 5], [10, 10]], [5, 5], axis=0] >>> X = X + np.random.randn[*X.shape] # 2 distinct "blobs" >>> centroids = np.array[[[5, 5], [10, 10]]] >>> X array[[[ 3.3955, 3.682 ], [ 5.9224, 5.785 ], [ 5.9087, 4.5986], [ 6.5796, 3.8713], [ 3.8488, 6.7029], [10.1698, 9.2887], [10.1789, 9.8801], [ 7.8885, 8.7014], [ 8.6206, 8.2016], [ 8.851 , 10.0091]]] >>> centroids array[[[ 5, 5], [10, 10]]]

Nói cách khác, chúng tôi muốn trả lời câu hỏi, mỗi điểm bên trong Xthuộc về centroid nào ? Chúng tôi cần thực hiện một số định hình lại để cho phép phát sóng ở đây, để tính toán khoảng cách Euclide giữa mỗi điểm trong Xvà mỗi điểm trongcentroids :

>>> >>> centroids[:, None] array[[[[ 5, 5]], [[10, 10]]]] >>> centroids[:, None].shape [2, 1, 2]

Điều này cho phép chúng tôi trừ một cách rõ ràng mảng này với mảng khác bằng cách sử dụng tích tổ hợp của các hàng của chúng :

>>> >>> np.linalg.norm[X - centroids[:, None], axis=2].round[2] array[[[2.08, 1.21, 0.99, 1.94, 2.06, 6.72, 7.12, 4.7 , 4.83, 6.32], [9.14, 5.86, 6.78, 7.02, 6.98, 0.73, 0.22, 2.48, 2.27, 1.15]]]

Nói cách khác, hình dạng NumPy của X - centroids[:, None]là [2, 10, 2], về cơ bản đại diện cho hai mảng xếp chồng lên nhau mà mỗi kích thước của X. Tiếp theo, chúng tôi muốn nhãn [số chỉ mục] của mỗi tâm gần nhất, tìm khoảng cách tối thiểu trên trục 0 từ mảng ở trên:

>>> >>> np.argmin[np.linalg.norm[X - centroids[:, None], axis=2], axis=0] array[[0, 0, 0, 0, 0, 1, 1, 1, 1, 1]]

Bạn có thể đặt tất cả những điều này lại với nhau ở dạng hàm:

>>> >>> def get_labels[X, centroids] -> np.ndarray: ... return np.argmin[np.linalg.norm[X - centroids[:, None], axis=2], ... axis=0] >>> labels = get_labels[X, centroids]

Hãy kiểm tra điều này một cách trực quan, vẽ biểu đồ của cả hai cụm và các nhãn được chỉ định của chúng bằng ánh xạ màu:

>>> >>> c1, c2 = ['#bc13fe', '#be0119'] # //xkcd.com/color/rgb/ >>> llim, ulim = np.trunc[[X.min[] * 0.9, X.max[] * 1.1]] >>> _, ax = plt.subplots[figsize=[5, 5]] >>> ax.scatter[*X.T, c=np.where[labels, c2, c1], alpha=0.4, s=80] >>> ax.scatter[*centroids.T, c=[c1, c2], marker='s', s=95, ... edgecolor='yellow'] >>> ax.set_ylim[[llim, ulim]] >>> ax.set_xlim[[llim, ulim]] >>> ax.set_title['One K-Means Iteration: Predicted Classes']

Bảng phân bổ

Vectorization cũng có ứng dụng trong tài chính.

Với lãi suất hàng năm, tần suất thanh toán [số lần mỗi năm], số dư khoản vay ban đầu và thời hạn cho vay, bạn có thể tạo một bảng phân bổ với các khoản thanh toán và số dư khoản vay hàng tháng, theo kiểu vectơ. Trước tiên, hãy đặt một số hằng số vô hướng:

>>> >>> freq = 12 # 12 months per year >>> rate = .0675 # 6.75% annualized >>> nper = 30 # 30 years >>> pv = 200000 # Loan face value >>> rate /= freq # Monthly basis >>> nper *= freq # 360 months

NumPy được cài đặt sẵn một số hàm tài chính , không giống như những người anh em họ Excel của chúng , có khả năng tạo ra kết quả đầu ra vectơ.

Con nợ [hoặc bên thuê] trả một số tiền cố định hàng tháng bao gồm cả gốc và lãi. Khi số dư nợ chưa thanh toán giảm, phần lãi của tổng số tiền phải trả sẽ giảm theo.

>>> >>> periods = np.arange[1, nper + 1, dtype=int] >>> principal = np.ppmt[rate, periods, nper, pv] >>> interest = np.ipmt[rate, periods, nper, pv] >>> pmt = principal + interest # Or: pmt = np.pmt[rate, nper, pv]

Tiếp theo, bạn sẽ cần tính toán số dư hàng tháng, cả trước và sau khoản thanh toán của tháng đó, có thể được xác định là giá trị tương lai của số dư ban đầu trừ đi giá trị tương lai của một niên kim [một dòng thanh toán], sử dụng hệ số chiết khấu d :

Về mặt chức năng, điều này trông giống như:

>>> >>> def balance[pv, rate, nper, pmt] -> np.ndarray: ... d = [1 + rate] ** nper # Discount factor ... return pv * d - pmt * [d - 1] / rate

Cuối cùng, bạn có thể thả nó vào định dạng bảng với Pandas DataFrame . Hãy cẩn thận với các dấu hiệu ở đây. PMTlà một dòng chảy ra từ quan điểm của con nợ.

>>> >>> import pandas as pd >>> cols = ['beg_bal', 'prin', 'interest', 'end_bal'] >>> data = [balance[pv, rate, periods - 1, -pmt], ... principal, ... interest, ... balance[pv, rate, periods, -pmt]] >>> table = pd.DataFrame[data, columns=periods, index=cols].T >>> table.index.name = 'month' >>> with pd.option_context['display.max_rows', 6]: ... # Note: Using floats for $$ in production-level code = bad ... print[table.round[2]] ... beg_bal prin interest end_bal month 1 200000.00 -172.20 -1125.00 199827.80 2 199827.80 -173.16 -1124.03 199654.64 3 199654.64 -174.14 -1123.06 199480.50 ... ... ... ... ... 358 3848.22 -1275.55 -21.65 2572.67 359 2572.67 -1282.72 -14.47 1289.94 360 1289.94 -1289.94 -7.26 -0.00

Vào cuối năm 30, khoản vay được trả hết:

>>> >>> final_month = periods[-1] >>> np.allclose[table.loc[final_month, 'end_bal'], 0] True

Lưu ý : Mặc dù việc sử dụng float để đại diện cho tiền có thể hữu ích cho việc minh họa khái niệm trong môi trường kịch bản, nhưng việc sử dụng float trong Python để tính toán tài chính trong môi trường sản xuất có thể khiến phép tính của bạn bị lệch một hoặc hai xu trong một số trường hợp.

Trích xuất tính năng hình ảnh

Trong một ví dụ cuối cùng, chúng tôi sẽ làm việc với hình ảnh tháng 10 năm 1941 của USS Lexington [CV-2], xác tàu được phát hiện ngoài khơi bờ biển Australia vào tháng 3 năm 2018. Đầu tiên, chúng tôi có thể ánh xạ hình ảnh thành mảng NumPy trong số các giá trị pixel của nó:

>>> >>> from skimage import io >>> url = ['//www.history.navy.mil/bin/imageDownload?image=/' ... 'content/dam/nhhc/our-collections/photography/images/' ... '80-G-410000/80-G-416362&rendition=cq5dam.thumbnail.319.319.png'] >>> img = io.imread[url, as_grey=True] >>> fig, ax = plt.subplots[] >>> ax.imshow[img, cmap='gray'] >>> ax.grid[False]

Vì lý do đơn giản, hình ảnh được tải ở thang độ xám, dẫn đến mảng 2d 64 bit nổi hơn là mảng MxNx4 RGBA 3 chiều , với các giá trị thấp hơn biểu thị các điểm tối hơn:

>>> >>> img.shape [254, 319] >>> img.min[], img.max[] [0.027450980392156862, 1.0] >>> img[0, :10] # First ten cells of the first row array[[0.8078, 0.7961, 0.7804, 0.7882, 0.7961, 0.8078, 0.8039, 0.7922, 0.7961, 0.7961]] >>> img[-1, -10:] # Last ten cells of the last row array[[0.0784, 0.0784, 0.0706, 0.0706, 0.0745, 0.0706, 0.0745, 0.0784, 0.0784, 0.0824]]

Một kỹ thuật thường được sử dụng như một bước trung gian trong phân tích hình ảnh là trích xuất bản vá . Như tên của nó, điều này bao gồm việc trích xuất các mảng con chồng lên nhau nhỏ hơn từ một mảng lớn hơn và có thể được sử dụng trong các trường hợp thuận lợi để "khử nhiễu" hoặc làm mờ hình ảnh.

Khái niệm này cũng mở rộng sang các lĩnh vực khác. Ví dụ: bạn đang làm điều gì đó tương tự bằng cách sử dụng các cửa sổ “luân phiên” của chuỗi thời gian với nhiều tính năng [biến]. Nó thậm chí còn hữu ích cho việc xây dựng Trò chơi cuộc sống của Conway . [Mặc dù, tích chập với nhân 3x3 là một cách tiếp cận trực tiếp hơn.]

Ở đây, chúng ta sẽ tìm thấy giá trị trung bình của mỗi bản vá 10x10 chồng chéo trong img. Lấy một ví dụ thu nhỏ, mảng bản vá 3x3 đầu tiên ở góc trên bên trái của imgsẽ là:

>>> >>> img[:3, :3] array[[[0.8078, 0.7961, 0.7804], [0.8039, 0.8157, 0.8078], [0.7882, 0.8 , 0.7961]]] >>> img[:3, :3].mean[] 0.7995642701525054

Cách tiếp cận thuần Python để tạo các bản vá trượt sẽ liên quan đến vòng lặp lồng nhau. Bạn cần phải xem xét rằng chỉ mục bắt đầu của các bản vá lỗi ngoài cùng bên phải sẽ là chỉ mục n - 3 + 1, ở đó nlà chiều rộng của mảng. Nói cách khác, nếu bạn đang giải nén các bản vá 3x3 từ một mảng 10x10 được gọi arr, bản vá cuối cùng được thực hiện sẽ là từ arr[7:10, 7:10]. Cũng nên nhớ rằng Python range[]không bao gồm stoptham số của nó :

>>> >>> size = 10 >>> m, n = img.shape >>> mm, nn = m - size + 1, n - size + 1 >>> >>> patch_means = np.empty[[mm, nn]] >>> for i in range[mm]: ... for j in range[nn]: ... patch_means[i, j] = img[i: i+size, j: j+size].mean[] >>> fig, ax = plt.subplots[] >>> ax.imshow[patch_means, cmap='gray'] >>> ax.grid[False]

Với vòng lặp này, bạn đang thực hiện rất nhiều lệnh gọi Python.

Một giải pháp thay thế có thể mở rộng thành hình ảnh RGB hoặc RGBA lớn hơn là NumPy's stride_tricks.

Bước đầu tiên mang tính hướng dẫn là hình dung, với kích thước bản vá và hình dạng hình ảnh, một mảng các bản vá lỗi có chiều cao hơn sẽ trông như thế nào. Chúng tôi có một mảng 2d imgvới hình dạng [254, 319]và một [10, 10]bản vá 2d. Điều này có nghĩa là hình dạng đầu ra của chúng ta [trước khi lấy giá trị trung bình của mỗi mảng 10x10 "bên trong" ] sẽ là:

>>> >>> shape = [img.shape[0] - size + 1, img.shape[1] - size + 1, size, size] >>> shape [245, 310, 10, 10]

Bạn cũng cần chỉ định các bước của mảng mới. Các bước của một mảng là một loạt các byte để nhảy trong mỗi chiều khi di chuyển dọc theo mảng. Mỗi pixel trong imglà một float 64 bit [8 byte], có nghĩa là tổng kích thước hình ảnh là 254 x 319 x 8 = 648.208 byte.

>>> >>> img.dtype dtype['float64'] >>> img.nbytes 648208

Bên trong, imgđược lưu giữ trong bộ nhớ dưới dạng một khối 648.208 byte liền kề. stridesdo đó là một loại thuộc tính giống như “siêu dữ liệu” cho chúng ta biết chúng ta cần bao nhiêu byte để chuyển tới vị trí tiếp theo dọc theo mỗi trục . Chúng tôi di chuyển theo khối 8 byte dọc theo các hàng nhưng cần phải di chuyển 8 x 319 = 2,552 byte để di chuyển "xuống" từ hàng này sang hàng khác.

>>> >>> img.strides [2552, 8]

Trong trường hợp của chúng tôi, các bước của các bản vá kết quả sẽ chỉ lặp lại các bước imghai lần:

>>> >>> strides = 2 * img.strides >>> strides [2552, 8, 2552, 8]

Bây giờ, hãy ghép những mảnh này lại với nhau bằng NumPy's stride_tricks:

>>> >>> from numpy.lib import stride_tricks >>> patches = stride_tricks.as_strided[img, shape=shape, strides=strides] >>> patches.shape [245, 310, 10, 10]

Đây là bản vá 10x10 đầu tiên :

>>> >>> patches[0, 0].round[2] array[[[0.81, 0.8 , 0.78, 0.79, 0.8 , 0.81, 0.8 , 0.79, 0.8 , 0.8 ], [0.8 , 0.82, 0.81, 0.79, 0.79, 0.79, 0.78, 0.81, 0.81, 0.8 ], [0.79, 0.8 , 0.8 , 0.79, 0.8 , 0.8 , 0.82, 0.83, 0.79, 0.81], [0.8 , 0.79, 0.81, 0.81, 0.8 , 0.8 , 0.78, 0.76, 0.8 , 0.79], [0.78, 0.8 , 0.8 , 0.78, 0.8 , 0.79, 0.78, 0.78, 0.79, 0.79], [0.8 , 0.8 , 0.78, 0.78, 0.78, 0.8 , 0.8 , 0.8 , 0.81, 0.79], [0.78, 0.77, 0.78, 0.76, 0.77, 0.8 , 0.8 , 0.77, 0.8 , 0.8 ], [0.79, 0.76, 0.77, 0.78, 0.77, 0.77, 0.79, 0.78, 0.77, 0.76], [0.78, 0.75, 0.76, 0.76, 0.73, 0.75, 0.78, 0.76, 0.77, 0.77], [0.78, 0.79, 0.78, 0.78, 0.78, 0.78, 0.77, 0.76, 0.77, 0.77]]]

Bước cuối cùng là khó khăn. Để có được giá trị trung bình được vector hóa của mỗi mảng 10x10 bên trong , chúng ta cần suy nghĩ cẩn thận về kích thước của những gì chúng ta có bây giờ. Kết quả sẽ thu gọn hai thứ nguyên cuối cùng để chúng ta chỉ còn lại một mảng 245x310 duy nhất .

Một cách [tối ưu] sẽ là định hình lại patchestrước tiên, làm phẳng các mảng 2d bên trong thành các vectơ có độ dài 100, và sau đó tính giá trị trung bình trên trục cuối cùng:

>>> >>> veclen = size ** 2 >>> patches.reshape[*patches.shape[:2], veclen].mean[axis=-1].shape [245, 310]

Tuy nhiên, bạn cũng có thể chỉ định axisdưới dạng một bộ giá trị, tính toán giá trị trung bình trên hai trục cuối cùng, sẽ hiệu quả hơn việc định hình lại:

>>> >>> patches.mean[axis=[-1, -2]].shape [245, 310]

Hãy đảm bảo rằng điều này được kiểm tra bằng cách so sánh bình đẳng với phiên bản lặp lại của chúng tôi. Nó không:

>>> >>> strided_means = patches.mean[axis=[-1, -2]] >>> np.allclose[patch_means, strided_means] True

Nếu khái niệm về những bước tiến khiến bạn chảy nước miếng, đừng lo lắng: Scikit-Learn đã nhúng toàn bộ quy trình này một cách độc đáo vào trong feature_extractionmô-đun của nó .

Suy nghĩ chia tay: Đừng tối ưu hóa quá mức

Trong bài viết này, chúng tôi đã thảo luận về việc tối ưu hóa thời gian chạy bằng cách tận dụng lợi thế của lập trình mảng trong NumPy. Khi bạn đang làm việc với các tập dữ liệu lớn, điều quan trọng là phải lưu ý đến hiệu suất vi mô.

Tuy nhiên, có một số trường hợp không thể tránh được vòng lặp Python nguyên bản. Như Donald Knuth đã khuyên , "Tối ưu hóa sớm là gốc rễ của mọi điều xấu xa." Các lập trình viên có thể dự đoán không chính xác vị trí nút cổ chai sẽ xuất hiện trong mã của họ, dành hàng giờ để cố gắng biểu diễn hóa đầy đủ một hoạt động sẽ dẫn đến cải thiện tương đối không đáng kể trong thời gian chạy.

Không có gì sai với các vòng lặp được rắc ở đây và ở đó. Thông thường, có thể hiệu quả hơn khi nghĩ về việc tối ưu hóa luồng và cấu trúc của toàn bộ tập lệnh ở mức độ trừu tượng cao hơn.

Nhiêu tai nguyên hơn

Tài liệu NumPy:

Sách:

Các nguồn lực khác:

Video liên quan

Chủ Đề