Съдържание на курса

Тензори от нулата (4): сумиране по лист

От · основател и главен изпълнителен директор на Hábil ·

Обновено на 21 октомври 2026 г.

Суми, средни стойности и най-добрият ден от седмицата, извадени от тензор на един ред, и правилото, което никога не се проваля: оста, по която сумираш, изчезва.

Тензори от нулата · урок A, сесия 4 от 8

Сценарий: героите и историята са измислени. Техническите и историческите данни и реалните цифри са с посочен източник.

Понеделник, 23 ноември, 7:40 вечерта. Складът на клона Centro.

Металната ролетка вече беше спусната наполовина. Миришеше на нов картон и на претоплено кафе. Матео беше сложил лаптопа върху кашон с тетрадки на редове, а доня Лупе, права зад него, още не беше свалила пуловера си.

— Остават трийсет дни — каза тя. — Господин Ортега пак ми се обади. Иска „нещо конкретно“.

— Почти съм готов, лельо.

— Същото каза и миналия понеделник. И по-миналия.

Матео не отговори. Цяла седмица режеше тензора на книжарницата: тук един ден, там един продукт. Беше видял странни неща. Петъкът в Norte, вял като вторник. Раниците, които не мърдат. Но това бяха парчета. А с парчета банка не се убеждава.

— Трябва да видя всичко заедно — каза той. — Не един ден. Не един продукт. Цялата седмица на всеки клон, в едно-единствено число.

— И компютърът може ли това?

— Може, на един ред. Ако му кажа точно какво да събере.

Доня Лупе седна върху обърната кофа от боя.

— Казвам ти го още отсега, момчето ми: Norte е на булеварда, до автобусната спирка. През онази врата влизат двойно повече хора, отколкото през тази. Ако продава по-малко от тук, искам да знам с колко по-малко.

Матео сложи пръсти на клавиатурата. Досега се беше научил да изважда части от тензора. Днес му предстоеше обратното: да обобщава. Да събира много числа в едно-единствено, по оста, която сам избере.

Отвън мина автобус и ролетката потрепери.

— Хайде тогава — каза доня Лупе. — Нека твоят тензор проговори.

Това е последната сесия от урок A. Ще направиш същото като Матео, стъпка по стъпка.

Преговор за начало

Отговори, без да поглеждаш сесия 3. Отговорите са в края, в решенията.

  1. Каква форма има papeleria[:, 4] и какво представлява?
  2. Каква е разликата във формата между papeleria[0] и papeleria[0:1]?
  3. Как изваждаш продажбите на раници на двата клона за всички дни?

Кодът на тази сесия е в публичното хранилище на курса, в папката sesion-04: github.com/HabilMX/curso-tensores. Първо го напиши сам, защото така се учи; хранилището е за сравнение, когато нещо не ти излезе по същия начин. Всяка програма идва с изхода, който трябва да получиш.

Цели на тази сесия

В края ще можеш да правиш тези четири неща. До всяко е упражнението, което го доказва.

# В края можеш… Доказва го
1 Да предвидиш каква форма остава, след като сумираш тензор по една или няколко оси, преди да изпълниш кода Упражнение 14
2 Да изчислиш суми и средни стойности по ос със sum и mean и да поправиш грешката на mean с цели числа Упражнения 15 и 17
3 Да намериш най-голямата или най-малката стойност по една ос и на коя позиция е, с max, min, argmax и argmin Упражнения 16 и 17
4 Да провериш всеки резултат с тензори срещу същото изчисление, направено с for Упражнения 15, 16 и 17

Какво ти трябва

  • Виртуалната среда от сесия 2, с инсталиран PyTorch, активирана.
  • Файл papeleria.py с данните на доня Лупе. Ако вече го имаш от сесия 3, използвай него. Ако не, копирай го така:
import torch

# eje 0 = sucursal (0 Centro, 1 Norte)
# eje 1 = día      (0 lunes ... 6 domingo)
# eje 2 = producto (0 cuadernos, 1 plumas, 2 mochilas)
centro = [
    [1200, 800, 500],    # lunes
    [1500, 900, 700],    # martes
    [1000, 700, 500],    # miércoles
    [1300, 900, 600],    # jueves
    [2000, 1200, 700],   # viernes
    [1200, 800, 400],    # sábado
    [800, 500, 300],     # domingo
]
norte = [
    [300, 200, 100],
    [400, 250, 150],
    [350, 200, 100],
    [300, 150, 100],
    [500, 300, 200],
    [300, 200, 100],
    [200, 100, 100],
]
papeleria = torch.tensor([centro, norte])

Всяка стъпка днес е нов файл в същата папка, който започва с from papeleria import .... Така не копираш данните всеки път.


Защо е важно

Тензорът пази подробностите. Всяка клетка, всеки ден, всеки продукт. Но подробностите нито обвиняват някого, нито спасяват някого: полезни са отговорите. Колко е продал всеки клон? В кой ден се продава най-много? Колко влиза средно на ден?

Матео забеляза нещо тази вечер: трите въпроса са една и съща операция. Събираш много числа в едно, по едно направление. В Excel това е колоната със суми в края на всеки ред или редът със суми отдолу. При тензора се нарича редукция, защото след операцията остават по-малко числа.

И това не е трик само за книжарници. Системите с изкуствен интелект редуцират постоянно. Ако банка има покупките на 1,000 клиенти за 30 дни, с 5 стойности за всяка покупка (форма (1000, 30, 5), както в сесия 1), и осредни по дните, получава как обикновено купува всеки клиент: таблица (1000, 5). Покупка, която се отклонява много от тази средна стойност, е тази, която си струва да се провери. Точно това прави господин Ортега от другата страна на телефона с терминала в Norte.

В магазините се случва същото, в голям мащаб. През 2020 г. международно състезание по прогнозиране, наречено M5, поиска да се предскажат дневните продажби на 10 магазина на американска верига супермаркети (публични данни от състезанието M5 в Kaggle) в три щата на САЩ: 42,840 времеви реда с продажби, организирани по нива (източник: M5 Forecasting - Accuracy, Kaggle; резултати в International Journal of Forecasting, doi:10.1016/j.ijforecast.2021.11.013). Сумирането по една ос, както ще правиш днес, е начинът да минеш от едно ниво към по-горното.

Случаят: книжарницата на доня Лупе, отново

Тензорът papeleria има форма (2, 7, 3): 2 клона, 7 дни, 3 продукта. Това са същите 42 числа като винаги. Тази вечер Матео ще им зададе въпросите, които наистина не дават на доня Лупе да спи.

Стъпка 1 — Сумата на всичко

Матео започна с най-простото, за загрявка: колко са продали двата клона заедно за цялата седмица? Създай paso1.py:

from papeleria import papeleria

total = papeleria.sum()
print(total)
print(total.shape)
print(total.item())

Изход:

tensor(23100)
torch.Size([])
23100

sum() без нищо вътре събира всичките 42 числа в едно-единствено: 23,100 песо. Обърни внимание на формата: torch.Size([]), празна. Това е скалар, като () от сесия 1, а .item() ти го дава като обикновено число на Python. Сметката излиза: 18,500 от Centro плюс 4,600 от Norte.

— Двайсет и три хиляди и сто? — доня Лупе сви устни. — Това го знам, момчето ми. Това ми го казва и тетрадката.

Беше права. Едно-единствено число скрива всичко. Тя иска да знае колко е продал всеки клон, всеки ден, от всеки продукт. За това казваш на sum по коя ос да събира.

Стъпка 2 — Един лист: сумиране по ширина и по дължина

Преди да се захване с целия куб, Матео направи онова, което би направил всеки детектив, който бърза, но не иска да сгреши: изпробва на позната територия. Един-единствен лист, този на Centro. Таблица от 7 дни × 3 продукта. При таблица има два начина да сумираш и ги познаваш от Excel:

             Cuad.  Plumas  Moch.   │ total del día
   Lun       1200    800    500     │  2500
   Mar       1500    900    700     │  3100
   ...                              │  ...
   Dom        800    500    300     │  1600
   ─────────────────────────────────┘
   total     9000   5800   3700
   del producto
  • По ширина (колоната вдясно): събираш трите продукта на всеки ден. Остават 7 числа, по едно за ден.
  • По дължина (редът отдолу): събираш седемте дни на всеки продукт. Остават 3 числа, по едно за продукт.

В PyTorch казваш кое от двете с dim=, което означава измерение, тоест ос. Спомни си как се номерират осите на листа: ос 0 са дните (редовете), а ос 1 са продуктите (колоните).

Създай paso2.py:

from papeleria import papeleria, centro

pestana_centro = papeleria[0]          # la pestaña del Centro: 7 días × 3 productos
print(pestana_centro.shape)

por_dia = pestana_centro.sum(dim=1)    # suma a lo largo del eje 1 (productos)
print(por_dia)
print(por_dia.shape)

# Lo mismo con for, como ya sabes hacerlo
por_dia_con_for = []
for renglon in centro:
    por_dia_con_for.append(sum(renglon))
print(por_dia_con_for)

assert por_dia.shape == (7,), "Esperaba un número por día: forma (7,)"
assert por_dia.tolist() == por_dia_con_for, "La suma con tensores no coincide con la del for"
print("Por día: cuadra")

Изход:

torch.Size([7, 3])
tensor([2500, 3100, 2200, 2800, 3900, 2400, 1600])
torch.Size([7])
[2500, 3100, 2200, 2800, 3900, 2400, 1600]
Por día: cuadra

— Тези числа ги знам наизуст — каза доня Лупе. — Понеделник две хиляди и петстотин, вторник три хиляди и сто…

Точно така. Това е списъкът por_dia, който тя продиктува на Матео в сесия 1. Тензорът и for дават едно и също. Първата проверка е мината: на позната територия инструментът не лъже.

Сега обърни внимание на формата, която е най-важното за днес. Влезе с (7, 3). Сумира с dim=1. Излезе с (7,). Тройката, която беше ос 1, изчезна. Логично: събра трите продукта в едно, така че вече няма „ос на продуктите“.

Нов инструмент в този код: .tolist(). Той е братовчед на .item(): .item() превръща тензор с едно-единствено число в число на Python, а .tolist() превръща тензор с няколко числа в списък на Python. Използваме го, за да може assert да сравни списък със списък.

Сега по дължина. В терминала на Python, с from papeleria import papeleria:

>>> papeleria[0].sum(dim=0)
tensor([9000, 5800, 3700])
>>> papeleria[0].sum(dim=0).shape
torch.Size([3])

Влезе с (7, 3), сумира с dim=0 и излезе с (3,): седмицата, която беше ос 0, изчезна. Събра седемте дни и остана по една сума за продукт. (В упражнение 15 ще провериш нещо подобно с for.)

Матео се загледа в двете форми. (7,). (3,). И написа на лепящо листче, което залепи на капака на лаптопа, правилото на цялата сесия. Научи го точно така:

Оста, която редуцираш, изчезва от формата. Останалите остават, в същия ред.

Трик, за да не се объркаш: dim= не казва какво ти остава, а какво събираш. dim=1 не е „дай ми продуктите“, а „събери продуктите“. Затова продуктите изчезват.

— Значи каквото събереш, то си отива — обобщи доня Лупе. — Като коледния бонус.

Стъпка 3 — Целият куб: три начина да сумираш

А сега целият куб. (2, 7, 3): три оси, значи има три начина да сумираш само по една. Матео още не изпълни нищо. Първо взе химикалка — от тези, които продава леля му — и предвиди всяка форма на една салфетка. Направи го и ти, с правилото: задраскай числото на оста, която събираш.

Събираш Коя ос Форма, която остава Какво означава
клоновете dim=0 2, 7, 3 → (7, 3) всеки ден и продукт, с двата клона заедно
дните dim=1 2, 7, 3 → (2, 3) всеки клон и продукт, за цялата седмица
продуктите dim=2 2, 7, 3 → (2, 7) всеки клон и ден, с всички продукти

Сега го провери. Създай paso3.py:

from papeleria import papeleria

sin_sucursal = papeleria.sum(dim=0)   # junta las dos pestañas
print(sin_sucursal.shape)
print(sin_sucursal)

sin_dia = papeleria.sum(dim=1)        # junta los 7 días
print(sin_dia.shape)
print(sin_dia)

sin_producto = papeleria.sum(dim=2)   # junta los 3 productos
print(sin_producto.shape)
print(sin_producto)

Изход:

torch.Size([7, 3])
tensor([[1500, 1000,  600],
        [1900, 1150,  850],
        [1350,  900,  600],
        [1600, 1050,  700],
        [2500, 1500,  900],
        [1500, 1000,  500],
        [1000,  600,  400]])
torch.Size([2, 3])
tensor([[9000, 5800, 3700],
        [2350, 1400,  850]])
torch.Size([2, 7])
tensor([[2500, 3100, 2200, 2800, 3900, 2400, 1600],
        [ 600,  800,  650,  550, 1000,  600,  400]])

Разчети ги, както би го направила доня Лупе:

  • dim=0 слага единия лист върху другия и сумира клетка по клетка. В понеделник, от тетрадки, Centro е продал 1,200, а Norte 300: заедно 1,500. Това е първата клетка. Клоновете изчезнаха: все едно доня Лупе има един-единствен голям магазин.
  • dim=1 събира седмицата. Горният ред е Centro: 9,000 от тетрадки, 5,800 от химикалки, 3,700 от раници, същите като в стъпка 2. Долният е Norte. Дните изчезнаха.
  • dim=2 събира продуктите. Горният ред е por_dia на Centro, който вече познаваш; долният — този на Norte. Продуктите изчезнаха.

Трите предвиждания от салфетката излязоха верни. Това е предимството на правилото: знаеш формата, преди да изпълниш кода. А ако излезе форма, различна от предвидената, знаеш, че си събрал грешната ос.

Но Матео вече не гледаше формите. Гледаше долния ред на dim=1. 2350, 1400, 850. Цялата седмица на Norte, продукт по продукт. Не стигаше дори колкото Centro продава само от тетрадки.

Оставаше му една сметка. Тази, за която беше дошъл.

Стъпка 4 — Събиране на две оси наведнъж: сумата на всеки клон

Въпросът на доня Лупе, онзи за вратата, през която влизат двойно повече хора: колко е продал всеки клон? За това трябва да събереш дните и продуктите: две оси наведнъж. sum приема кортеж от оси. Създай paso4.py:

from papeleria import papeleria, centro, norte

por_sucursal = papeleria.sum(dim=(1, 2))   # junta días y productos
print(por_sucursal.shape)
print(por_sucursal)

por_sucursal_con_for = []
for pestana in [centro, norte]:
    total = 0
    for renglon in pestana:
        for venta in renglon:
            total = total + venta
    por_sucursal_con_for.append(total)
print(por_sucursal_con_for)

assert por_sucursal.shape == (2,), "Esperaba un número por sucursal: forma (2,)"
assert por_sucursal.tolist() == por_sucursal_con_for, "No coincide con el for"
assert por_sucursal[0].item() == 18500, "El Centro debería sumar 18,500"
print("Por sucursal: cuadra")

Изход:

torch.Size([2])
tensor([18500,  4600])
[18500, 4600]
Por sucursal: cuadra

18,500 Centro и 4,600 Norte.

Числата бяха в таблицата още от сесия 1. Но никой не ги беше слагал така, едно до друго, с вратата на Norte наум.

Доня Лупе се приближи до екрана. Дълго време не каза нищо.

— Четири пъти — каза най-накрая. — Centro е продал около четири пъти повече от Norte; Norte — около една четвърт. А през неговата врата влизат двойно повече хора.

— Лельо, това не казва защо. Казва само колко.

— Аха. — Тя вече гледаше телефона си. — А кой прави касата на Norte всяка вечер, Матео?

Той знаеше отговора. Всички го знаеха. Фер.

Правилото междувременно си остава същото: 2, 7, 3 → (2,). Задраска две числа, изчезнаха две оси.

На for му трябваха три цикъла, един в друг. На тензора — един ред. И не само се пише по-кратко: при големи масиви for в Python може да отнеме много повече време от sum на PyTorch. Колко повече зависи от компютъра ти, от версията и от размера, така че го измери на своята машина, преди да цитираш цифра.

Внимавай, ако го правиш на две стъпки. Матео, със студени ръце, поиска да изкара сметката още веднъж по друг начин, за всеки случай: да сумира първо по едната ос, а после по другата. Но внимание: след първото сумиране осите се номерират наново. Виж този опит. Създай paso4c.py:

from papeleria import papeleria

paso_a = papeleria.sum(dim=1)     # quito los días: queda (2, 3)
print(paso_a.shape)
paso_b = paso_a.sum(dim=2)        # ¿quito los productos?

Изход:

torch.Size([2, 3])
Traceback (most recent call last):
  File ".../paso4c.py", line 5, in <module>
    paso_b = paso_a.sum(dim=2)        # ¿quito los productos?
             ^^^^^^^^^^^^^^^^^
IndexError: Dimension out of range (expected to be in range of [-2, 1], but got 2)

Разчети го дума по дума: „измерението е извън диапазона: очаквах нещо между -2 и 1, а ти ми даде 2“. След като махнеш дните, paso_a има форма (2, 3): само две оси, 0 и 1. Продуктите вече не са ос 2; сега са ос 1. (Отрицателните числа броят осите от края; засега не ти трябват.)

Лесното решение: направи двете неща с едно-единствено извикване, dim=(1, 2). Там числата винаги се отнасят до първоначалната форма.

Матео се върна към извикването на един ред. 4,600. Числото не помръдна.

В другия ъгъл на склада доня Лупе набираше номер. Остави го да звъни. Затвори. Набра отново.

Стъпка 5 — Средната стойност на ден и грешката, която със сигурност ще видиш

— Изкарай ми колко продава Norte в един нормален ден — каза доня Лупе, без да пуска телефона. — За да ѝ го кажа в очите.

Тоест: колко се продава средно на ден във всеки клон? Средната стойност се изчислява с mean (на английски „средно“). Работи също като sum, с dim=. Първо изваждаш продажбите за всеки ден (като събираш продуктите), а после осредняваш седемте дни. Създай paso5a.py:

from papeleria import papeleria

ventas_diarias = papeleria.sum(dim=2)       # (2, 7): sucursal × día
print(ventas_diarias.dtype)
promedio_diario = ventas_diarias.mean(dim=1)

Изход:

torch.int64
Traceback (most recent call last):
  File ".../paso5a.py", line 5, in <module>
    promedio_diario = ventas_diarias.mean(dim=1)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: mean(): could not infer output dtype. Input dtype must be either a floating point or complex dtype. Got: Long

Хвърли грешка.

— Сега пък какво? — попита доня Лупе.

— Нищо страшно, лельо. Оплаква се, че съм му дал цели числа.

— Ами като всички. И аз обичам целите.

Разчети го бавно, в превод:

  • mean(): could not infer output dtype: „mean не успя да реши от какъв тип да направи резултата“.
  • Input dtype must be either a floating point or complex dtype: „това, което влиза, трябва да е с десетични числа“ (floating point са числата с десетична точка; complex са друг вид числа, които тук няма да използваш).
  • Got: Long: „а ти ми даде Long“. Long е името, с което PyTorch нарича вътрешно int64, типа за цели числа, който видя в сесия 2. Затова отпечатахме dtype предварително: torch.int64.

Защо отказва? Защото средната стойност почти никога не излиза цяло число: 18,500 делено на 7 дава 2,642.857…, а тензорът с цели числа няма къде да запази „.857“. Вместо да го отреже тайно, PyTorch спира и те предупреждава. На Матео това му хареса. В тази история и без това се случваха твърде много неща тайно.

Има два начина да го поправиш.

Поправка 1: превръщане в десетични с .float(). Създай paso5b.py:

from papeleria import papeleria, centro

ventas_diarias = papeleria.sum(dim=2)       # (2, 7): sucursal × día
decimales = ventas_diarias.float()
print(decimales.dtype)
print(decimales)

promedio_diario = decimales.mean(dim=1)     # quita el eje de los días
print(promedio_diario.shape)
print(promedio_diario)

# Con for: el total del Centro entre 7 días
total_centro = 0
for renglon in centro:
    total_centro = total_centro + sum(renglon)
print(total_centro / 7)

assert promedio_diario.shape == (2,), "Esperaba un promedio por sucursal"
assert round(promedio_diario[0].item(), 2) == round(total_centro / 7, 2), "No coincide con el for"
print("Promedio diario: cuadra")

Изход:

torch.float32
tensor([[2500., 3100., 2200., 2800., 3900., 2400., 1600.],
        [ 600.,  800.,  650.,  550., 1000.,  600.,  400.]])
torch.Size([2])
tensor([2642.8572,  657.1429])
2642.8571428571427
Promedio diario: cuadra

.float() не променя числата, а само как се съхраняват: обърни внимание на точката след всяко от тях (2500.). Сега са float32, десетични. Centro продава средно за $2,642.86 на ден, а Norte — за $657.14.

— Шестстотин петдесет и седем песо — повтори доня Лупе. — С такъв ъгъл. Толкова продава и сергия за дъвки.

Два детайла в този код:

  • Формата: (2, 7) с dim=1 даде (2,). Дните изчезнаха, както винаги.
  • assert сравнява, като закръгля до два знака след десетичната точка, защото тензорът казва 2642.8572, а for — 2642.8571428571427. Нито едното не е грешно: тензорът използва float32, а Python дели с 64-битов float. И двете са двоични приближения, но float32 пази по-малка точност. Без закръгляне assert би се провалил, макар сметката да е вярна. Внимание: закръгляме само за да сравним или да покажем; за истинско счетоводство пази сумите в центове като цели числа или използвай десетична аритметика, а не float32.

Поправка 2: създаване на тензора с десетични числа от самото начало. Ако данните на доня Лупе идваха с десетични, тензорът щеше да се роди с десетични и mean щеше да работи направо (за точни пари е по-добре с цели центове). Създай paso5c.py:

import torch

con_centavos = torch.tensor([2500.0, 3100.5, 2200.0])
print(con_centavos.dtype)
print(con_centavos.mean())

Изход:

torch.float32
tensor(2600.1667)

Достатъчно е числата да имат десетична точка, за да избере PyTorch сам float32. За книжарницата, която вече съществува с цели числа, най-практична е поправка 1: .float() точно преди mean.

Стъпка 6 — Най-добрият ден: max и argmax

Матео имаше подозрение, различно от това на леля си, и искаше да го провери, преди да каже каквото и да е. Ако някой криеше продажби напосоки, Norte щеше да се държи странно: добри дни, когато не им е времето, мъртви дни на случаен принцип. Затова зададе последния въпрос за вечерта: в кой ден се продава най-много във всеки клон? Тук те интересуват два отговора: колко е продадено в този ден (стойността) и кой ден е бил (позицията). Създай paso6.py:

from papeleria import papeleria

ventas_diarias = papeleria.sum(dim=2)       # (2, 7): sucursal × día
resultado = ventas_diarias.max(dim=1)
print(resultado)

Изход:

torch.return_types.max(
values=tensor([3900, 1000]),
indices=tensor([4, 4]))

Изглежда странно, но е просто. Когато дадеш dim= на max, той не връща тензор, а кутийка с две неща:

  • values (стойности): най-високата продажба на всеки клон. 3,900 в Centro, 1,000 в Norte.
  • indices (индекси): на коя позиция по оста е била тази продажба. 4 и 4.

А коя е позиция 4 по оста на дните? Понеделник 0, вторник 1, сряда 2, четвъртък 3… петък. И двата клона продават най-много в петък.

Както при кортеж в Python, можеш да извадиш двете неща наведнъж, всяко в своя променлива. А със списък от имена превръщаш индекса в думи. Създай paso6b.py:

from papeleria import papeleria

dias = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
sucursales = ["Centro", "Norte"]

ventas_diarias = papeleria.sum(dim=2)
valores, indices = ventas_diarias.max(dim=1)
print(valores)
print(indices)

for s in range(2):
    dia = dias[indices[s].item()]
    print(sucursales[s], "vendió más el", dia, ":", valores[s].item())

Изход:

tensor([3900, 1000])
tensor([4, 4])
Centro vendió más el viernes : 3900
Norte vendió más el viernes : 1000

Правилото за формата важи и тук: влезе с (2, 7), събра дните с dim=1 и както valores, така и indices имат форма (2,): по едно за клон.

Матео се облегна на стола. Един и същ ден за двата. Norte не се държеше като неуправляем магазин. Държеше се като Centro, но в умален вид. Сякаш от всеки ден, горе-долу равномерно, липсваше по едно парче.

Това не прилича на някой, който взима пари от касата през ден. Прилича на нещо друго. Но на какво? Матео още нямаше с какво да го разбере, затова не каза нищо.

В упражнение 16 ще провериш такъв резултат срещу for: по-дълго е, но вече знаеш как да го напишеш.

Роднините му. Ако искаш само позицията, без стойността, използвай argmax („аргументът на максимума“, тоест къде се намира). Ако искаш най-ниската стойност, min и argmin работят по същия начин. А ако махнеш dim, max търси в целия тензор и връща едно-единствено число. Създай paso6c.py:

from papeleria import papeleria

ventas_diarias = papeleria.sum(dim=2)
print(ventas_diarias.argmax(dim=1))
print(ventas_diarias.max())
print(ventas_diarias.min(dim=1))

Изход:

tensor([4, 4])
tensor(3900)
torch.return_types.min(
values=tensor([1600,  400]),
indices=tensor([6, 6]))

Петъкът е най-добрият ден и на двата клона; 3,900 е най-високата дневна продажба в целия тензор; а най-слабият ден и на двата е 6, неделята. Същият модел, отново. Norte копира Centro дори в лошите си дни.

Една разлика, която ще ти спести време: sum и mean приемат кортеж от оси, като dim=(1, 2); max и min — не, само по една наведнъж. Ще го видиш в таблицата с грешките.

За когато ти потрябва: keepdim. Ако добавиш keepdim=True към която и да е от тези функции, оста, която събираш, не изчезва, а остава с дължина 1: papeleria.sum(dim=2, keepdim=True) дава форма (2, 7, 1) вместо (2, 7). Днес не ти трябва; в урок B ще видиш кога е полезно.

Всичко в една таблица

Беше почти девет. Матео преписа на един-единствен лист всичко, което беше изкарал тази вечер, за да може доня Лупе да го занесе на господин Ортега. Всеки въпрос — един ред:

Въпрос на доня Лупе Код Форма
Колко съм продала общо? papeleria.sum() ()
Колко е продал всеки клон? papeleria.sum(dim=(1, 2)) (2,)
Колко е продал всеки клон всеки ден? papeleria.sum(dim=2) (2, 7)
Колко е продал всеки клон от всеки продукт? papeleria.sum(dim=1) (2, 3)
Колко съм продала от всеки продукт всеки ден, с двата клона заедно? papeleria.sum(dim=0) (7, 3)
Колко продавам средно на ден във всеки клон? papeleria.sum(dim=2).float().mean(dim=1) (2,)
В кой ден продавам най-много във всеки клон? papeleria.sum(dim=2).argmax(dim=1) (2,)

PyTorch и TensorFlow: същата сума, с друго име

Преди да затвори лаптопа, Матео искаше да е сигурен, че 4,600 не е каприз на PyTorch. Изпълни същата сметка в TensorFlow, другия голям инструмент. Там функциите се казват reduce_sum и reduce_mean (ето я думата „редуцирам“), а оста се казва axis вместо dim. Този блок е за четене, не за изпълнение: курсът не инсталира TensorFlow.

import tensorflow as tf
from papeleria import centro, norte

papeleria_tf = tf.constant([centro, norte])
print(tf.reduce_sum(papeleria_tf, axis=(1, 2)))
print(tf.reduce_mean(tf.reduce_sum(papeleria_tf, axis=2), axis=1))

Изход (TensorFlow 2.21):

tf.Tensor([18500  4600], shape=(2,), dtype=int32)
tf.Tensor([2642  657], shape=(2,), dtype=int32)

Сумата е същата: 4,600 и в двата инструмента. Но виж средната стойност: 2642 и 657, без десетични и без никакво предупреждение. TensorFlow запазва целочисления тип и прави целочислено деление: отрязва дробната част; PyTorch предпочете да хвърли грешка. Грешката поне те предупреждава; криво число, което никой не забелязва, е по-лошо. Ако използваш TensorFlow, преобразувай в десетични, преди да осредняваш:

promedios_tf = tf.reduce_mean(
    tf.cast(tf.reduce_sum(papeleria_tf, axis=2), tf.float32),
    axis=1,
)
print(promedios_tf)

Изход (TensorFlow 2.21):

tf.Tensor([2642.8572  657.1429], shape=(2,), dtype=float32)

Матео го записа и го подчерта два пъти. Трийсет дни преди краен срок число, което се променя без звук, е последното, от което имаш нужда.


👉 Ако идваш от бизнес администрацията: онова, което Матео направи тази вечер, ти го правиш с обобщена таблица (pivot table). Когато сложиш „клон“ в редовете и „сума на продажбите“ в стойностите, Excel събира дните и продуктите и ти оставя по едно число за клон: това е papeleria.sum(dim=(1, 2)). Ако сложиш „ден“ в редовете, събира клоновете и продуктите: papeleria.sum(dim=(0, 2)). Правилото от днес важи и там: каквото не сложиш в обобщената таблица, изчезва от отчета. И урокът от стъпка 5 също: преди да представиш средна стойност, провери дали инструментът ти не е отрязал центовете. И още един, който обобщената таблица няма да ти каже: ниската сума ти казва колко липсва, никога кой е виновен.


Упражнения към сесия 4

Решенията са в края на този брой. Първо опитай сам; assert ще ти каже дали си познал. Винаги в един и същ ред: първо формата, после стойността.

Упражнение 14 — Предвиди формата. Напиши на хартия каква форма остава във всеки случай и какво означава тя за доня Лупе. После превърни всяко предвиждане в assert и го изпълни. Първото е решено:

from papeleria import papeleria

assert papeleria.sum(dim=0).shape == (7, 3)          # ejemplo resuelto
assert papeleria.sum(dim=1).shape == ____
assert papeleria.sum(dim=2).shape == ____
assert papeleria.sum(dim=(0, 1)).shape == ____
assert papeleria.sum(dim=(0, 2)).shape == ____
assert papeleria.sum(dim=(0, 1, 2)).shape == ____
print("Ejercicio 14 superado")

Допълнителен въпрос: как другояче можеш да напишеш papeleria.sum(dim=(0, 1, 2))?

Упражнение 15 — Средната стойност по продукт (наполовина). Доня Лупе иска да знае колко продава средно на ден от всеки продукт във всеки клон. Попълни двете празни места:

Префиксът f пред кавичките създава f-string: позволява да вмъкнеш стойност във фигурни скоби в текста. Тук той ще накара съобщението на assert да покаже истинската форма, ако нещо се провали.

from papeleria import papeleria, norte

promedio = papeleria.____().mean(dim=____)
print(promedio)

assert promedio.shape == (2, 3), f"Esperaba (sucursal, producto) = (2, 3) y salió {tuple(promedio.shape)}"

suma_plumas_norte = 0
for renglon in norte:
    suma_plumas_norte = suma_plumas_norte + renglon[1]

assert round(promedio[1, 1].item(), 2) == round(suma_plumas_norte / 7, 2), "No coincide con el for"
print("Ejercicio 15 superado")

Обърни внимание на съобщението на първия assert: ако сбъркаш оста, то ти казва каква форма ти е излязла. Тази форма ти казва по коя ос си събрал по погрешка.

Упражнение 16 — Най-доброто и най-лошото от седмицата. Кода го пишеш ти. За всеки въпрос: assert за формата и после стойността.

а) Кой е най-слабият ден на всеки клон и колко е продадено в този ден? Провери отговора за Norte с for. б) Кой е звездният продукт (този, от който е продадено най-много за седмицата) на всеки клон? в) С двата клона заедно, в кой ден е продадено най-много и колко? Трябва да излезе 4,900.

Подсказка: при всеки въпрос първо реши кои оси събираш за сумата и после по коя ос търсиш.

Упражнение 17 — Твоите оценки. Върни се към оценките от упражнение 4 на сесия 1. Ако не си запазил своите, използвай тези (ученик × предмет × изпит):

parciales = [
    [[8, 9, 9], [7, 8, 9], [9, 9, 10], [6, 7, 8]],
    [[10, 9, 10], [9, 9, 9], [8, 8, 9], [9, 10, 10]],
    [[7, 7, 8], [8, 8, 8], [6, 7, 7], [8, 9, 9]],
]

Превърни ги в тензор и изкарай, всяко със своя assert за формата:

а) Общата средна оценка на всеки ученик. б) Средната оценка на всеки ученик по всеки предмет. в) Кой ученик има най-добрата средна оценка. г) Кой е най-трудният предмет за групата (този с най-ниска средна оценка, като събереш учениците и изпитите).

Провери подточка а) за първия ученик с for. Ако си използвал свои данни с друг ред на осите, твоите dim ще са различни от тези в решението, и това е наред: важното е формата, която предвиждаш, да е тази, която излиза.


Чести грешки (и как да ги забележиш)

Грешка Как изглежда Как да я забележиш Какво да направиш
Мислиш, че dim казва какво ти остава Искаш сума по ден и пишеш dim=1, като мислиш „оста на дните“ Формата излиза (2, 3) вместо (2, 7) dim казва какво събираш. За сума по ден събери другите оси
Осредняваш цели числа RuntimeError: mean(): could not infer output dtype. ... Got: Long dtype казва torch.int64 .float() преди mean или данни с десетична точка
Сумираш на две стъпки със старата номерация IndexError: Dimension out of range (expected to be in range of [-2, 1], but got 2) Поискал си ос, която вече не съществува След редукция осите се номерират наново. По-добре едно-единствено извикване: dim=(1, 2)
Сравняваш тензор със списък assert por_dia == [2500, 3100, ...] хвърля AssertionError, макар числата да са верни assert се проваля с числа, които на око съвпадат Първо преобразувай: por_dia.tolist() == [...]
Сравняваш десетични числа без закръгляне 2642.857177734375 == 18500 / 7 дава False assert се проваля заради последната цифра Закръгли двете страни: round(x, 2)
Даваш кортеж на max TypeError: max() received an invalid combination of arguments - got (dim=tuple, ), ... Написал си max(dim=(1, 2)) max и min приемат само една ос. Първо сумирай или редуцирай по едната и после търси по другата

Самооценка за урок A

Завърши първия урок. Преди да минеш към урок B, провери дали можеш да направиш всяко от тези неща, без да поглеждаш сесиите. Ако нещо ти е трудно, върни се към сесията, която е посочена, и повтори упражнението ѝ.

Сесия 1 — Едно число, един списък, една таблица, един куб

  • Казвам колко измерения има един набор от данни и какво представлява всяко от тях.
  • Записвам тези данни като вложени списъци на Python и посочвам число с индексите му.
  • Изчислявам формата ръчно и казвам колко числа има общо, като я умножа.
  • Разпознавам неправоъгълен списък и обяснявам защо не може да бъде тензор.
  • Пиша assert със съобщение, за да проверявам собствената си работа.

Сесия 2 — Твоят първи тензор

  • Превръщам списък на Python в тензор с torch.tensor.
  • Разчитам shape, ndim, dtype и numel() на тензор и обяснявам какво казва всяко от тях.
  • Знам какъв dtype избира PyTorch за цели и за десетични числа.

Сесия 3 — Осите имат имена

  • Изваждам число с papeleria[1, 4, 2] и го превръщам в число на Python с .item().
  • Изваждам цял срез по която и да е ос, като papeleria[:, 4] или papeleria[:, :, 2], и казвам формата му, преди да изпълня кода.

Сесия 4 — Сумиране по лист

  • Предвиждам формата след редукция по една или няколко оси.
  • Изчислявам суми и средни стойности по ос със sum и mean и поправям грешката на mean с цели числа.
  • Намирам най-голямата, най-малката стойност и позицията им с max, min, argmax и argmin.
  • Проверявам резултат с тензори срещу същото изчисление с for.

Преговор на урок A

Десет въпроса от четирите сесии. Отговори на тях, без да поглеждаш нищо; отговорите са в решенията.

  1. Какво е правилото, за да разбереш колко измерения има един набор от данни?
  2. Ако доня Лупе пази продажбите за 4 седмици, по клон, ден и продукт, каква форма има тензорът и колко числа съдържа?
  3. Какво прави assert, когато условието е вярно, и какво прави, когато е невярно?
  4. Какво става, ако дадеш на torch.tensor списъка [[1, 2, 3], [4, 5]]?
  5. Какво ти казват papeleria.ndim, papeleria.numel() и papeleria.dtype?
  6. Какъв dtype има torch.tensor([1, 2, 3])? А torch.tensor([1.5, 2.0])?
  7. Какво число дава papeleria[1, 4, 2] и за какво служи да му добавиш .item()?
  8. Каква форма има papeleria[:, :, 2] и какво представлява?
  9. Каква форма оставя papeleria.sum(dim=0) и какво означава тя за доня Лупе?
  10. Какво връща max(dim=1), което sum(dim=1) не връща?

Преговор за следващата сесия

Сесия 5 започва с тези три въпроса. Отговори на тях, без да поглеждаш урока:

  1. Ако сумираш papeleria с dim=1, каква форма остава и какво означава тя?
  2. Защо mean се проваля с цели числа и как го поправяш?
  3. Как проверяваш, че сумата ти с тензори е вярна?

🔁 Следващата седмица започва урок B, Тензорите в действие. На Матео ще му се наложи да погледне по-назад от една седмица: цели месеци. А за да ги види, ще трябва да промени формата на тензора, без да променя данните му: същите числа, подредени в други кутии. Ще разчетеш, дума по дума, и грешката, която излиза, когато новата форма не пасва.

Преди да спусне ролетката, доня Лупе показа на Матео телефона си. Беше поискала от Фер отчета на касата за седмицата още от четири часа. След това дойдоха обажданията: следобедните и тези от тази вечер. Нито едно не беше върнато. Матео я помоли да почака, защото числата казват колко, а не кой. Тя прибра телефона, без да му отговори.

Фер не вдигна телефона през целия следобед.


Решения (не ги гледай, преди да опиташ)

Преговор за начало (от сесия 3).

  1. papeleria[:, 4] има форма (2, 3): трите продукта в петък във всеки клон.
  2. papeleria[0] има форма (7, 3): листът Centro, без оста на клоновете. papeleria[0:1] има форма (1, 7, 3): срез, който запазва оста на клоновете, с един-единствен клон вътре.
  3. papeleria[:, :, 2]: всички клонове, всички дни, продукт 2 (раници). Форма (2, 7).

Упражнение 14.

assert papeleria.sum(dim=0).shape == (7, 3)          # cada día y producto, sucursales juntas
assert papeleria.sum(dim=1).shape == (2, 3)          # cada sucursal y producto, en la semana
assert papeleria.sum(dim=2).shape == (2, 7)          # cada sucursal y día, productos juntos
assert papeleria.sum(dim=(0, 1)).shape == (3,)       # cada producto, en todo
assert papeleria.sum(dim=(0, 2)).shape == (7,)       # cada día, en todo
assert papeleria.sum(dim=(0, 1, 2)).shape == ()      # un solo número: el total

Ако отпечаташ и стойностите:

tensor([11350,  7200,  4550])
tensor([3100, 3900, 2850, 3350, 4900, 3000, 2000])
tensor(23100)
Ejercicio 14 superado

Допълнителен въпрос: papeleria.sum(dim=(0, 1, 2)) е същото като papeleria.sum(). Да събереш всички оси означава да събереш всичко: 23,100.

Упражнение 15. Празните места са float и 1: преобразуваш в десетични и събираш дните.

promedio = papeleria.float().mean(dim=1)

Изход:

tensor([[1285.7142,  828.5714,  528.5714],
        [ 335.7143,  200.0000,  121.4286]])
Ejercicio 15 superado

Norte продава средно точно за $200 химикалки на ден (1,400 делено на 7). Ако си сложил dim=0, съобщението на assert ти е казало:

AssertionError: Esperaba (sucursal, producto) = (2, 3) y salió (7, 3)

(7, 3) те издава: изчезнаха клоновете, а не дните.

Упражнение 16.

from papeleria import papeleria, norte

dias = ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
productos = ["cuadernos", "plumas", "mochilas"]

# a) Día más flojo: junto productos (2, 7) y busco el mínimo a lo largo de los días
valores, indices = papeleria.sum(dim=2).min(dim=1)
assert valores.shape == (2,), "Esperaba un valor por sucursal"
assert valores.tolist() == [1600, 400], "Los mínimos deberían ser 1,600 y 400"
assert indices.tolist() == [6, 6], "El domingo debería ser el día más flojo"
print(valores, indices)

peor_dia = 0
for d in range(7):
    if sum(norte[d]) < sum(norte[peor_dia]):
        peor_dia = d
assert indices[1].item() == peor_dia, "No coincide con el for"

# b) Producto estrella: junto días (2, 3) y busco la posición del máximo en productos
estrella = papeleria.sum(dim=1).argmax(dim=1)
assert estrella.shape == (2,), "Esperaba un producto por sucursal"
assert estrella.tolist() == [0, 0], "Los cuadernos deberían ser el producto estrella"
print(productos[estrella[0].item()], productos[estrella[1].item()])

# c) Mejor día con las dos sucursales juntas: junto sucursales y productos (7,)
por_dia_juntas = papeleria.sum(dim=(0, 2))
assert por_dia_juntas.shape == (7,), "Esperaba un total por día"
print(dias[por_dia_juntas.argmax().item()], por_dia_juntas.max().item())
assert por_dia_juntas.max().item() == 4900, "El mejor día debería sumar 4,900"
assert por_dia_juntas.argmax().item() == 4, "El mejor día debería ser viernes"
print("Ejercicio 16 superado")

Изход:

tensor([1600,  400]) tensor([6, 6])
cuadernos cuadernos
viernes 4900
Ejercicio 16 superado

Неделята е най-слабият ден и на двата клона (1,600 и 400), тетрадките са звездният продукт и на двата, а в петък, с двата клона заедно, са продадени 4,900. Във в) argmax() без dim върши работа, защото por_dia_juntas вече има само една ос.

Упражнение 17.

import torch

parciales = torch.tensor([
    [[8, 9, 9], [7, 8, 9], [9, 9, 10], [6, 7, 8]],
    [[10, 9, 10], [9, 9, 9], [8, 8, 9], [9, 10, 10]],
    [[7, 7, 8], [8, 8, 8], [6, 7, 7], [8, 9, 9]],
])                                   # alumno × materia × parcial
decimales = parciales.float()

por_alumno = decimales.mean(dim=(1, 2))           # a) quito materias y parciales
assert por_alumno.shape == (3,)
por_alumno_y_materia = decimales.mean(dim=2)      # b) quito parciales
assert por_alumno_y_materia.shape == (3, 4)
por_materia = decimales.mean(dim=(0, 2))          # d) quito alumnos y parciales
assert por_materia.shape == (4,)

mejor_alumno = por_alumno.argmax()                # c)
assert mejor_alumno.shape == ()
assert mejor_alumno.item() == 1, "El segundo alumno debería tener el mejor promedio"
assert torch.allclose(por_materia, torch.tensor([8.5556, 8.3333, 8.1111, 8.4444]), atol=1e-4)
assert por_materia.argmin().item() == 2, "La tercera materia debería ser la más difícil"  # d)

print(por_alumno)
print(por_alumno_y_materia)
print(mejor_alumno)
print(por_materia, por_materia.argmin())

suma = 0
cuantas = 0
for materia in parciales[0].tolist():
    for calificacion in materia:
        suma = suma + calificacion
        cuantas = cuantas + 1
assert round(por_alumno[0].item(), 2) == round(suma / cuantas, 2), "No coincide con el for"
print("Ejercicio 17 superado")

Изход:

tensor([8.2500, 9.1667, 7.6667])
tensor([[8.6667, 8.0000, 9.3333, 7.0000],
        [9.6667, 9.0000, 8.3333, 9.6667],
        [7.3333, 8.0000, 6.6667, 8.6667]])
tensor(1)
tensor([8.5556, 8.3333, 8.1111, 8.4444]) tensor(2)
Ejercicio 17 superado

Вторият ученик (индекс 1) има най-добрата средна оценка, 9.17, а най-трудният предмет е третият (индекс 2), с 8.11. Ако си забравил .float(), видял си същия RuntimeError от стъпка 5: оценките също са цели числа.

Преговор на урок A.

  1. Броят на измеренията е броят на стойностите, които ти трябват, за да посочиш едно-единствено число.
  2. (4, 2, 7, 3): седмица, клон, ден и продукт. Съдържа 4 × 2 × 7 × 3 = 168 числа.
  3. Ако условието е вярно, не прави нищо и програмата продължава. Ако е невярно, спира програмата с AssertionError и показва съобщението ти.
  4. PyTorch отказва да го създаде, защото списъкът не е правоъгълен (един ред с 3 и друг с 2): ValueError: expected sequence of length 3 at dim 1 (got 2).
  5. ndim дава 3 (три оси), numel() дава 42 (общия брой числа, 2 × 7 × 3), а dtype дава torch.int64 (цели числа).
  6. torch.int64 за целите числа и torch.float32 за десетичните.
  7. Дава 200: Norte, петък, раници. .item() го превръща от тензор с едно число в обикновено число на Python, за да го отпечаташ или да го сравниш в assert.
  8. (2, 7): продажбите на раници на двата клона за седемте дни.
  9. (7, 3): събира двата клона; остава всеки ден и всеки продукт, все едно доня Лупе има един-единствен магазин.
  10. Освен най-високата стойност връща и на коя позиция е тя (индексите). sum връща само сумите.

Какво следва

С това завършваш урок A: знаеш какво е тензор, как да го създадеш, да го обхождаш по осите му и да го обобщаваш. В сесия 5 започва урок B. Преди да стигнеш дотам, реши цялата самооценка; ако всичко ти излиза, без да поглеждаш, продължаваш със солидна основа.


Пиша от онова, което виждаме всеки ден в Hábil, където изграждаме системи за банки, застрахователи и компании в Мексико. Ако в работата ти или в семейния ти бизнес се сблъскаш с подобен проблем, нека поговорим: habil.mx

Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx