Skip to content

7. Управление памятью 1D массивов: Представления vs Копии ​

Анатомия Views и Copies в NumPy ​

Как NumPy экономит оперативную память на срезах ​

Главная архитектурная особенность NumPy, обеспечивающая его колоссальную скорость при работе с Big Data и AI, заключается в механизме представлений (Views).

Когда вы делаете стандартный срез массива (например, sub_array = array[1:4]), NumPy не копирует данные в новое место памяти и не создает независимый объект. Вместо этого он создает новый заголовок массива (объект-оболочку), который ссылается на тот же самый исходный непрерывный блок памяти (C-contiguous buffer), просто с измененными смещениями (offset) и шагом (stride). Это позволяет нарезать гигабайтные датасеты за наносекунды без лишних затрат RAM.

Что такое представление (View) и побочные эффекты (Side-effects) ​

Поскольку срез является лишь «окном» (представлением) в исходный массив, любое изменение элементов внутри этого среза неявно изменяет данные в исходном массиве.

python
import numpy

# Original array representing raw model logits
logits = numpy.array([1.2, 3.4, -0.5, 4.1])

# Creating a view via basic slicing
logits_view = logits[1:3]

# Modifying the view affects the original data!
logits_view[0] = 99.0
print(logits)  # Outputs modified original: [ 1.2, 99.0, -0.5,  4.1]

Такое поведение называется побочным эффектом (side-effect). Оно может стать причиной скрытых и трудноуловимых багов в пайплайнах предобработки данных.

Проверка связи данных через атрибут .base ​

Чтобы программно определить, является ли массив самостоятельной копией или просто представлением другого объекта, используется атрибут .base.

  • Если массив владеет своей памятью (это копия или исходный массив), .base вернет None.
  • Если массив является представлением, .base вернет ссылку на исходный корневой объект массива.
python
import numpy

parent_array = numpy.array([10, 20, 30])
view_array = parent_array[:]

print(parent_array.base)                # Outputs: None
print(view_array.base)                  # Outputs: [10 20 30]
print(view_array.base is parent_array)  # Outputs: True

Безопасное копирование через метод .copy() ​

Если вам необходимо изолировать данные среза от исходного датасета (например, выделить чистую тестовую выборку для экспериментов), нужно использовать метод .copy(). Он выполняет глубокое копирование (Deep Copy) — выделяет новый независимый блок памяти на C-уровне и копирует туда значения элементов.

python
import numpy

source_data = numpy.array([1, 2, 3])
isolated_copy = source_data[:2].copy()

isolated_copy[0] = -999
print(source_data)          # Outputs untouched original: [1, 2, 3]
print(isolated_copy.base)   # Outputs: None (owns its memory)

При работе с изолированными копиями данных критически важно соблюдать правильный порядок операций. Ошибочный выбор синтаксиса может привести к избыточному выделению оперативной памяти (RAM) и замедлению работы программы на больших датасетах.

Рассмотрим две строки, выполняющие одну и ту же задачу — изоляцию первых двух элементов массива:

python
# Вариант 1: Сначала срез, потом копия (РЕКОМЕНДУЕМЫЙ)
isolated_copy = source_data[:2].copy()

# Вариант 2: Сначала копия, потом срез (НЕОПТИМАЛЬНЫЙ)
isolated_copy = source_data.copy()[:2]

В чем разница под капотом? ​

  1. source_data[:2].copy(): NumPy сначала создает мгновенное «окно» (view) на первые два элемента исходного массива без дублирования данных. Метод .copy() вызывается только для этого маленького окна. В результате в памяти выделяется место ровно под 2 элемента.
  2. source_data.copy()[:2]: NumPy сначала полностью дублирует в памяти весь исходный массив (со всеми его элементами, которых в реальных задачах могут быть миллионы). И только после создания этой тяжелой независимой копии от нее отрезаются первые два элемента. Оставшаяся часть данных мгновенно отбрасывается, приводя к неэффективному расходу ресурсов процессора и RAM.

Ниже представлен самостоятельный пример кода, который демонстрирует полную независимость созданной копии от оригинального массива данных:

python
import numpy

# 1. Initialize the original array
source_data = numpy.array([1, 2, 3])

# 2. Extract a safe, isolated copy of the first two elements
# Note: In production, always use the syntax: source_data[:2].copy()
isolated_copy = source_data[:2].copy()

# 3. Modify the isolated copy to verify independence
isolated_copy[0] = -999

# 4. Check the results
print(source_data)         # Outputs untouched original: [1, 2, 3]
print(isolated_copy.base)  # Outputs: None (owns its memory)

Пояснения к выводу в консоль:

  • print(source_data) выводит [1, 2, 3]. Изменение элемента isolated_copy[0] = -999 никак не затронуло исходный вектор source_data. Это доказывает, что массивы не делят общие ячейки памяти.
  • print(isolated_copy.base) возвращает None. Свойство .base указывает на родительский объект, если текущий массив является лишь временным «окном» (view). Значение None гарантирует, что массив isolated_copy полностью автономен, обладает собственным буфером данных и сам владеет своей памятью.

Безопасная предобработка датасетов ​

Риски неявного изменения данных в AI-пайплайнах ​

В реальных задачах ИИ данные проходят многоступенчатые цепочки трансформаций (аугментация, нормализация, фильтрация аномалий). Если на этапе разбиения данных на мини-батчи или валидационные выборки вы будете использовать обычные срезы, последующая нормализация (например, зануление пропусков или деление на максимальное значение) затрет или необратимо исказит ваш исходный сырой датасет, загруженный с диска.

Правило безопасного копирования сеток данных ​

При работе со сгенерированными сетками (например, linspace для расписаний темпа обучения) или при вычленении кусков из тренировочного датасета всегда задавайте себе вопрос: «Будут ли эти данные модифицироваться дальше по коду?». Если ответ «Да» — применение .copy() строго обязательно.

Пример 1. Опасный пайплайн (Без копирования) — Искажение исходного датасета

В данном примере подмножество данных выделяется через обычный срез. Последующая обработка этого подмножества скрыто портит оригинальный массив, что может привести к неверному обучению модели.

python
import numpy as np

# Simulate a raw dataset loaded from disk (e.g., pixel intensities)
raw_dataset = np.array([255.0, 128.0, 64.0, 0.0, 192.0])

# Extract a validation batch using a simple slice (creates a view)
validation_batch = raw_dataset[0:3]

# Normalize the batch (In-place modification)
validation_batch /= 255.0

# CRITICAL BUG: The raw source dataset is now corrupted!
print("Corrupted Raw Dataset:")
print(raw_dataset)  # Outputs: [1.  0.50196078 0.25098039 0.  192. ]

Пример 2. Безопасный пайплайн (С использованием .copy()) — Полная изоляция

Здесь метод .copy() изолирует извлеченный батч. Оригинальные сырые данные остаются нетронутыми, и их можно повторно использовать в других ветках AI-пайплайна.

python
import numpy as np

# Simulate a raw dataset loaded from disk
raw_dataset = np.array([255.0, 128.0, 64.0, 0.0, 192.0])

# Extract and isolate the validation batch safely using .copy()
validation_batch = raw_dataset[0:3].copy()

# Normalize the isolated batch
validation_batch /= 255.0

print("Processed Batch:")
print(validation_batch)  # Outputs: [1.  0.50196078 0.25098039]

print("\nSafe Untouched Raw Dataset:")
print(raw_dataset)       # Outputs original values: [255. 128.  64.   0. 192.]

Упражнения базовые (5 шт) (только с опорой на объясненную в данном уроке теорию) ​

1. Тестирование связи среза и оригинала ​

Задача: Создайте вектор original_tensor из чисел [1.0, 2.0, 3.0, 4.0]. Сделайте срез первых двух элементов в переменную tensor_view. Измените первый элемент в tensor_view на значение 0.0. Выведите на экран original_tensor и tensor_view и посмотрите, как изменился оригинал.

Посмотреть решение
python
import numpy

original_tensor = numpy.array([1.0, 2.0, 3.0, 4.0])
tensor_view = original_tensor[:2]

# Modifying the view
tensor_view[0] = 0.0

# In-place change inspection
print("Original tensor after view mutation:", original_tensor)  # [0. 2. 3. 4.]
print("View tensor after view mutation:", tensor_view)          # [0. 2. 3. 4.]

2. Программа контроля памяти с атрибутом .base ​

Задача: Создайте массив base_dataset из диапазона от 0 до 5. Сделайте его срез с шагом 2 в переменную sampled_view. Напишите логическую проверку (через if-else), которая проверяет атрибут .base у sampled_view и выводит сообщение "This is a View", если массив ссылается на память оригинала, или "This is a Copy".

Посмотреть решение
python
import numpy

base_dataset = numpy.arange(6)
sampled_view = base_dataset[::2]

# Checking the reference to memory
if sampled_view.base is not None:
    print("This is a View")
else:
    print("This is a Copy")

3. Изоляция тестовой выборки через .copy() ​

Задача: Дан исходный массив признаков features = numpy.array([100, 200, 300, 400]). Создайте независимую тестовую выборку test_features, скопировав последние два элемента с помощью .copy(). Замените все элементы в test_features на 0 и выведите features, чтобы доказать отсутствие побочных эффектов.

Посмотреть решение
python
import numpy

features = numpy.array([100, 200, 300, 400])

# Creating a safe independent copy
test_features = features[-2:].copy()
test_features[:] = 0

print("Source features (must remain untouched):", features)  # [100 200 300 400]
print("Test features base attribute:", test_features.base)    # None

4. Модификация полной копии массива ​

Задача: Создайте вектор чисел от 10 до 15. Создайте его полную копию с помощью метода .copy() (без использования срезов). Докажите, что у полученной копии атрибут .base возвращает None.

Посмотреть решение
python
import numpy

source_array = numpy.arange(10, 16)
cloned_array = source_array.copy()

print("Is base None?", cloned_array.base is None)  # Outputs: True

5. Скрытая мутация при обратном срезе ​

Задача: Сделайте инвертированный срез reversed_view = original[:: -1] для массива original = numpy.array([1, 2, 3]). Измените последний элемент в reversed_view на 99. Выведите исходный массив original и определите, создают ли инвертированные срезы копию или представление.

Посмотреть решение
python
import numpy

original = numpy.array([1, 2, 3])
reversed_view = original[::-1]

# Mutating the reversed slice
reversed_view[-1] = 99

# Even reversed slices are views under the hood!
print("Original array:", original)  # Outputs: [99, 2, 3]

Упражнения смешанные (5 шт) (включают материал прошлых уроков) ​

6. Безопасная аугментация квантованного батча ​

Задача: Создайте вектор меток классов из 6 элементов [1, 2, 3, 4, 5, 6] типа numpy.int8 (Теория Урока 4). Выделите из него последние 3 элемента в виде независимой безопасной копии (Теория Урока 6). Замените в этой копии все элементы на 0. Выведите исходный квантованный вектор.

Посмотреть решение
python
import numpy

# Generating quantized array (Lesson 4)
labels_int8 = numpy.array([1, 2, 3, 4, 5, 6], dtype=numpy.int8)

# Safe slice isolation (Lesson 5 & Lesson 6)
isolated_batch = labels_int8[-3:].copy()
isolated_batch[:] = 0

print("Original labels vector:", labels_int8)  # [1 2 3 4 5 6]

7. Анализ памяти и связи для сгенерированного расписания LR ​

Задача: Сгенерируйте сетку темпа обучения из 5 элементов от 0.1 до 0.01 через linspace (Урок 3). Создайте представление sub_lr, взяв элементы со 2-го по 4-й (индексы 1, 2, 3). Проверьте объем памяти sub_lr в байтах (Урок 4) и выведите значение sub_lr.base is not None (Урок 6).

Посмотреть решение
python
import numpy

# Linear scheduler generation (Lesson 3)
lr_schedule = numpy.linspace(0.1, 0.01, 5)

# Extracting a sub-slice view (Lesson 5)
sub_lr = lr_schedule[1:4]

# Verification (Lesson 4 & Lesson 6)
print("Bytes consumed by sub-slice:", sub_lr.nbytes)
print("Is it a view linked to parent?", sub_lr.base is not None)  # True

8. Зануление пропусков NaN в изолированном векторе ​

Задача: Инициализируйте корректный вектор весов типа numpy.float32, содержащий [0.9, numpy.nan, 0.4] (Урок 4). Сделайте его полную изолированную копию (Урок 6). В полученной копии замените элемент с индексом 1 (NaN) на рабочее значение 0.0 (Урок 5). Выведите исходный вектор с NaN и исправленную копию.

Посмотреть решение
python
import numpy

# Strict float32 initialization with safe NaN marker (Lesson 4)
raw_weights = numpy.array([0.9, numpy.nan, 0.4], dtype=numpy.float32)

# Deep copying (Lesson 6)
clean_weights = raw_weights.copy()

# Updating an element via index (Lesson 5)
clean_weights[1] = 0.0

print("Source weights with NaN:", raw_weights)   # [0.9, nan, 0.4]
print("Cleaned isolated weights:", clean_weights) # [0.9, 0.0, 0.4]

9. Проверка базового объекта для цепочки срезов ​

Задача: Сгенерируйте массив чисел от 0 до 9 с помощью numpy.arange (Урок 3). Сделайте первый срез view_one = array[2:8]. Затем сделайте срез от среза view_two = view_one[::2]. Выведите логическое сравнение view_two.base is array. Выясните в комментарии, к какому объекту ведет цепочка представлений.

Посмотреть решение
python
import numpy

# Initial generation (Lesson 3)
root_array = numpy.arange(10)

# Chain of views (Lesson 5)
view_one = root_array[2:8]
view_two = view_one[::2]

#NumPy tracks the very original block of memory on C-level (Lesson 6)
print("Does view_two link directly to root_array?", view_two.base is root_array)  # True

10. Оценка экономии памяти: Копия против Представления ​

Задача: Создайте большой вектор из 1 000 000 нулей типа numpy.float32 (Урок 4). Извлеките из него каждый второй элемент в vector_view, а затем сделайте то же самое, но с помощью метода vector_copy = array[::2].copy(). Прочитайте комментарий к решению, объясняющий фундаментальную разницу в потреблении памяти ОС между этими двумя операциями.

Посмотреть решение
python
import numpy
# 4 Megabytes in RAM (Lesson 4)
huge_tensor = numpy.zeros(1000000, dtype=numpy.float32)

# Slicing creates a View
vector_view = huge_tensor[::2]

# vector_view allocates almost 0 additional bytes in RAM, it just reuses huge_tensor buffer!Slicing with copy allocations

vector_copy = huge_tensor[::2].copy()

# vector_copy immediately allocates 2 new Megabytes of physical memory in RAM!
print("View base allocation exists:", vector_view.base is not None)
print("Copy base allocation exists:", vector_copy.base is not None)