Tensor Basics
What is a Tensor
Tensors are the core data structure in Riemann, essentially a 0-dimensional or multi-dimensional array used to quantify and describe objective things such as text, images, videos, audio, and more.
In Riemann, tensors have the following characteristics:
Multi-dimensional array structure: Supports 0-dimensional (scalar), 1-dimensional (vector), 2-dimensional (matrix), and higher-dimensional array representations
Mathematical operation support: Supports basic mathematical operations such as addition, subtraction, multiplication, division, inner product, and various common mathematical functions
Shape transformation capabilities: Supports tensor shape reshaping, dimension expansion/reduction, indexing and slicing operations
Automatic gradient tracking: Built-in automatic differentiation mechanism that supports gradient calculation and backpropagation
Device compatibility: Supports running on different devices such as CPU and GPU
Tensors are the foundation for building neural networks and gradient descent algorithms. The 0-dimensional scalars, 1-dimensional vectors, and 2-dimensional matrices you are familiar with in mathematics are all special forms of tensors.
It should be noted that tensors in Riemann are not exactly equivalent to tensors in tensor algebra or tensor analysis, mainly due to some differences in operation rules. The tensors mentioned here primarily serve neural network-related computations, and their essence is multi-dimensional arrays that support various operators and functions, as well as automatic gradient tracking.
Creating Tensors
From Data
You can create tensors directly from Python lists or NumPy arrays:
import riemann as rm
import numpy as np
# From Python list
x = rm.tensor([1, 2, 3])
print(x) # tensor([1, 2, 3])
# From NumPy array
np_array = np.array([1, 2, 3])
x = rm.tensor(np_array)
print(x) # tensor([1, 2, 3])
With Specific Data Types
You can specify the data type when creating tensors:
# Float32 tensor (default)
x = rm.tensor([1, 2, 3], dtype=rm.float32)
# Float64 tensor
x = rm.tensor([1, 2, 3], dtype=rm.float64)
# Complex tensor
x = rm.tensor([1+2j, 3+4j], dtype=rm.complex64)
With Specific Device
You can specify the device when creating tensors:
# CPU tensor (default)
x = rm.tensor([1, 2, 3], device='cpu')
# CUDA tensor
x = rm.tensor([1, 2, 3], device='cuda')
# Specify CUDA device index
x = rm.tensor([1, 2, 3], device='cuda:0')
With Gradient Tracking
You can specify whether gradient tracking is needed when creating tensors:
# No gradient tracking (default)
x = rm.tensor([1, 2, 3], requires_grad=False)
# With gradient tracking (only valid for float types)
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
Tensor Function Parameters
tensor function signature:
def tensor(data, dtype=None, device=None, requires_grad=False) -> TN
Parameter explanation:
data: Can be any data that can be converted to a numpy array, including lists, tuples, scalars, numpy arrays, etc.
dtype: Optional, specifies the data type of the tensor. If None, the data type is inferred from the input data.
device: Optional, specifies the device where the tensor is located, can be ‘cpu’, ‘cuda’, ‘cuda:0’, integer index, or Device object. If None, uses the current device context or default device.
requires_grad: Optional, boolean value, specifies whether gradient calculation is needed for this tensor, default is False.
Handling of None parameters:
When dtype is None:
If data is a numpy array or cupy array, preserves the original data type
If data is a Python scalar:
bool → bool
int → int64
float → default floating-point type (default: float32)
complex → default complex type (default: complex64)
If data is a Python list or tuple, infers data type based on element types (chooses the smallest type that can accommodate all elements)
When device is None:
First checks if in a CUDA device context
If in CUDA context, uses the current CUDA device
Otherwise uses the default device (default: CPU)
Usage examples:
# Basic usage
x = rm.tensor([1, 2, 3])
# Full parameter example
x = rm.tensor(
data=[1.0, 2.0, 3.0],
dtype=rm.float32,
device='cuda',
requires_grad=True
)
Querying and Setting Defaults:
Default data type:
# Get current default floating-point type
default_dtype = rm.get_default_dtype()
print(default_dtype) # Default is float32
# Set default floating-point type
rm.set_default_dtype(rm.float64)
print(rm.get_default_dtype()) # Now float64
Default device:
# Get current default device
default_device = rm.get_default_device()
print(default_device) # Default is device(type='cpu', index=None)
# Set default device
rm.set_default_device('cuda')
print(rm.get_default_device()) # Now device(type='cuda', index=0)
# Set specific CUDA device as default
rm.set_default_device('cuda:1')
print(rm.get_default_device()) # Now device(type='cuda', index=1)
Example: Creating tensors with default settings:
# Set default device to CUDA
rm.set_default_device('cuda')
# Set default data type to float64
rm.set_default_dtype(rm.float64)
# Create tensor without specifying device and dtype
# Will use default settings
x = rm.tensor([1.0, 2.0, 3.0])
print(x.device) # cuda:0
print(x.dtype) # float64
Data Type and Device Initialization
dtype object initialization:
Riemann supports multiple ways to initialize data types:
# Using Riemann built-in dtype
dtype = rm.float32
dtype = rm.float64
dtype = rm.int32
dtype = rm.int64
dtype = rm.complex64
dtype = rm.complex128
# Using NumPy dtype
import numpy as np
dtype = np.float32
dtype = np.dtype('float64')
# Using string
dtype = 'float32'
dtype = 'float64'
Device object initialization:
Riemann’s Device object can be initialized in the following ways:
# Using string
device = rm.device('cpu')
device = rm.device('cuda')
device = rm.device('cuda:0')
# Using integer index (CUDA only)
device = rm.device(0) # Equivalent to 'cuda:0'
# Through Device constructor
from riemann import Device
device = Device('cpu')
device = Device('cuda:1')
Device object attributes:
device = rm.device('cuda:0')
print(device.type) # 'cuda'
print(device.index) # 0
device = rm.device('cpu')
print(device.type) # 'cpu'
print(device.index) # None
Device Context Management
Riemann supports using context managers to temporarily switch devices. Tensors created inside the with block will use the specified device by default:
import riemann as rm
# Create tensor on CPU
x = rm.tensor([1, 2, 3])
print(x.device) # cpu
# Temporarily switch to CUDA device
with rm.device('cuda'):
# Create tensor on CUDA
y = rm.tensor([4, 5, 6])
print(y.device) # cuda:0
# When device parameter is not specified, uses context device
z = rm.tensor([7, 8, 9])
print(z.device) # cuda:0
# After exiting context, default device is restored
w = rm.tensor([10, 11, 12])
print(w.device) # cpu
Advantages of context management:
Avoids repeatedly specifying device parameter for each tensor creation
Ensures all tensors in the code block are on the same device
Automatically restores previous device state, avoiding device state confusion
Usage scenarios:
# Example: Execute compute-intensive operations on CUDA
with rm.device('cuda'):
# Create input tensor
input_data = rm.tensor([[1.0, 2.0], [3.0, 4.0]])
# Perform computation
result = rm.matmul(input_data, input_data)
# Result is automatically on CUDA
print(result.device) # cuda:0
Special Tensors
Riemann provides a rich set of special tensor creation functions. The table below lists all supported functions and their capabilities:
Function |
Description |
Example |
|---|---|---|
|
Creates a tensor filled with zeros |
|
|
Creates a tensor of zeros with the same shape as the input |
|
|
Creates a tensor filled with ones |
|
|
Creates a tensor of ones with the same shape as the input |
|
|
Creates an uninitialized tensor |
|
|
Creates an uninitialized tensor with the same shape as the input |
|
|
Creates a tensor filled with a specified value |
|
|
Creates a tensor filled with a specified value, with the same shape as the input |
|
|
Creates an identity matrix |
|
|
Creates a tensor with uniform distribution [0, 1) |
|
|
Creates a tensor with standard normal distribution |
|
|
Creates a tensor with random integers in a specified range |
|
|
Creates a tensor with normal distribution of specified mean and std |
|
|
Creates a tensor with random permutation of integers from 0 to n-1 |
|
|
Creates a 1D tensor with evenly spaced values |
|
|
Creates a 1D tensor with specified number of evenly spaced values |
|
|
Creates a tensor from a NumPy or CuPy array |
|
Usage examples:
# Zeros tensor
x = rm.zeros(3, 4)
# Ones tensor
x = rm.ones(2, 3)
# Identity matrix
x = rm.eye(3)
# Random tensor
x = rm.randn(2, 3) # Normal distribution
x = rm.rand(2, 3) # Uniform distribution [0, 1)
# Filled tensor
x = rm.full((2, 3), 5) # Creates 2x3 tensor filled with 5
# Sequence tensors
x = rm.arange(0, 10, 2) # 0, 2, 4, 6, 8
x = rm.linspace(0, 1, 5) # 0, 0.25, 0.5, 0.75, 1.0
# From NumPy array
import numpy as np
np_array = np.array([1, 2, 3])
x = rm.from_numpy(np_array)
Default Parameter Behavior for Special Tensors
When creating special tensors, if you don’t specify dtype and device parameters, different default behaviors are used based on the function type:
Tensor Creation Functions Without Reference Tensor (e.g., zeros, ones, rand, etc.):
When dtype and device parameters are not specified, the function behavior is consistent with the tensor() function
Default data type is float32
Default device is the current device context or default device setting
Reference Tensor-based “like” Functions (e.g., zeros_like, ones_like, etc.):
When dtype and device parameters are not specified, the dtype and device of the reference tensor are used to create the tensor
This ensures that the newly created tensor has the same data type and device as the reference tensor
Default Data Type (dtype):
# Default creates float32 tensor
x = rm.zeros(3, 4)
print(x.dtype) # float32
# Explicitly specify data type
x = rm.zeros(3, 4, dtype=rm.float64)
print(x.dtype) # float64
Default Device (device):
When not specifying the device parameter, the current device context or default device setting is used
This is consistent with the default behavior of the tensor() function
# Default uses current device context or default device
x = rm.zeros(3, 4)
print(x.device) # Defaults to cpu
# Create within CUDA context
with rm.device('cuda'):
x = rm.zeros(3, 4)
print(x.device) # cuda:0
# Explicitly specify device
x = rm.zeros(3, 4, device='cuda')
print(x.device) # cuda:0
Complete Parameter Example:
# Specify all parameters
x = rm.zeros(
3, 4, # Shape
dtype=rm.float32, # Data type
device='cuda', # Device
requires_grad=True # Gradient tracking
)
# Random tensor example
x = rm.randn(
2, 3, # Shape
dtype=rm.float64, # Data type
device='cpu', # Device
requires_grad=False # Gradient tracking
)
Tensor Attributes and States
Tensors have various attributes and state detection functions for obtaining basic information and detecting states.
Tensor Attributes
Attribute |
Description |
Example |
|---|---|---|
|
Tensor data type |
|
|
Device where tensor is located |
|
|
Number of tensor dimensions |
|
|
Tensor shape |
|
|
Size of tensor in specified dimension |
|
|
Total number of tensor elements |
|
|
Whether tensor is a leaf node in computation graph |
|
|
Whether tensor requires gradient tracking |
|
State Detection Functions
Function |
Description |
Example |
|---|---|---|
|
Detect if tensor is floating point type |
|
|
Detect if tensor is complex type |
|
|
Detect if tensor is real type |
|
|
Detect if tensor elements are infinite |
|
|
Detect if tensor elements are NaN |
|
|
Detect if tensor is on CUDA device |
|
|
Detect if tensor is on CPU device |
|
|
Get or set tensor data type |
|
|
Detect if tensor is stored contiguously |
|
Attributes and State Detection Examples
import riemann as rm
# Create a tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6]], dtype=rm.float32, requires_grad=True)
print("Original tensor:", x)
# 1. Basic attributes
print("\n1. Basic attributes:")
print("Data type:", x.dtype)
print("Device:", x.device)
print("Number of dimensions:", x.ndim)
print("Shape:", x.shape)
print("Size of dimension 0:", x.size(0))
print("Total number of elements:", x.numel())
print("Is leaf node:", x.is_leaf)
print("Requires gradient:", x.requires_grad)
# 2. State detection
print("\n2. State detection:")
print("Is floating point:", x.is_floating_point())
print("Is complex:", x.is_complex())
print("Is real:", x.isreal())
print("Is on CUDA:", x.is_cuda)
print("Is on CPU:", x.is_cpu)
print("Is contiguous:", x.is_contiguous())
# 3. Special value detection
print("\n3. Special value detection:")
y = rm.tensor([1.0, float('inf'), float('nan')])
print("Tensor:", y)
print("Is infinite:", y.isinf())
print("Is NaN:", y.isnan())
# 4. Type operations
print("\n4. Type operations:")
print("Current type:", x.type())
x_double = x.type(rm.float64)
print("Type after conversion:", x_double.type())
Tensor Computations
Basic Arithmetic
Tensors support standard arithmetic operations:
x = rm.tensor([1, 2, 3])
y = rm.tensor([4, 5, 6])
# Addition
z = x + y
# Subtraction
z = x - y
# Multiplication (element-wise)
z = x * y
# Division
z = x / y
# Matrix multiplication
a = rm.tensor([[1, 2], [3, 4]])
b = rm.tensor([[5, 6], [7, 8]])
c = a @ b # Matrix multiplication
Tensor Product Operations
Riemann supports various tensor product operations, each with different mathematical meanings and application scenarios:
1. Dot Product (Inner Product)
The dot product of two vectors is a scalar, calculated as:
import riemann as rm
a = rm.tensor([1, 2, 3])
b = rm.tensor([4, 5, 6])
# Using dot function
result = rm.dot(a, b) # tensor(32.)
# Or using einsum
result = rm.einsum('i,i->', a, b) # tensor(32.)
2. Outer Product
The outer product of two vectors is a matrix:
a = rm.tensor([1, 2, 3])
b = rm.tensor([4, 5])
# Using outer function
result = rm.outer(a, b) # shape: (3, 2)
# Or using einsum
result = rm.einsum('i,j->ij', a, b) # shape: (3, 2)
3. Hadamard Product (Element-wise Multiplication)
Element-wise product of two tensors with the same shape:
A = rm.tensor([[1, 2], [3, 4]])
B = rm.tensor([[5, 6], [7, 8]])
# Using * operator
C = A * B # tensor([[5, 12], [21, 32]])
# Or using einsum
C = rm.einsum('ij,ij->ij', A, B) # tensor([[5, 12], [21, 32]])
4. Kronecker Product
The Kronecker product of two tensors is a block matrix:
A = rm.tensor([[1, 2], [3, 4]])
B = rm.tensor([[0, 5], [6, 7]])
# Using kron function
C = rm.kron(A, B)
# tensor([[ 0, 5, 0, 10],
# [ 6, 7, 12, 14],
# [ 0, 15, 0, 20],
# [18, 21, 24, 28]])
5. Matrix Multiplication
Standard matrix multiplication:
A = rm.tensor([[1, 2], [3, 4]])
B = rm.tensor([[5, 6], [7, 8]])
# Using @ operator
C = A @ B # tensor([[19, 22], [43, 50]])
# Or using matmul function
C = rm.matmul(A, B)
# Or using einsum
C = rm.einsum('ij,jk->ik', A, B)
6. Cross Product
The cross product of two 3D vectors is a vector perpendicular to both input vectors:
a = rm.tensor([1., 2., 3.])
b = rm.tensor([4., 5., 6.])
# Using cross function
result = rm.cross(a, b) # tensor([-3., 6., -3.])
# Batch cross product
a_batch = rm.tensor([[1., 2., 3.], [4., 5., 6.]])
b_batch = rm.tensor([[4., 5., 6.], [1., 2., 3.]])
result = rm.cross(a_batch, b_batch) # shape: (2, 3)
Product Operations Comparison Table
Operation Type |
Input Shapes |
Output Shape |
Function/Operator |
|---|---|---|---|
Dot Product |
(n,), (n,) |
() |
|
Outer Product |
(m,), (n,) |
(m, n) |
|
Hadamard Product |
(m, n), (m, n) |
(m, n) |
|
Kronecker Product |
(m, n), (p, q) |
(m*p, n*q) |
|
Matrix Multiplication |
(m, n), (n, p) |
(m, p) |
|
Cross Product |
(3,), (3,) or (…, 3), (…, 3) |
(3,) or (…, 3) |
|
Mathematical Functions
Riemann provides a wide range of mathematical functions. Here are the commonly used ones:
Basic Mathematical Functions
Function |
Description |
Example |
|---|---|---|
|
Compute absolute value |
|
|
Compute square root |
|
|
Compute square |
|
|
Compute exponential function |
|
|
Compute 2 to the power |
|
|
Compute natural logarithm |
|
|
Compute base-10 logarithm |
|
|
Compute base-2 logarithm |
|
|
Compute sign function |
|
|
Round up |
|
|
Round down |
|
|
Round to nearest integer |
|
|
Truncate decimal part |
|
Trigonometric Functions
Function |
Description |
Example |
|---|---|---|
|
Compute sine |
|
|
Compute cosine |
|
|
Compute tangent |
|
|
Compute arcsine |
|
|
Compute arccosine |
|
|
Compute arctangent |
|
|
Compute arctangent of two tensors |
|
Hyperbolic Functions
Function |
Description |
Example |
|---|---|---|
|
Compute hyperbolic sine |
|
|
Compute hyperbolic cosine |
|
|
Compute hyperbolic tangent |
|
|
Compute inverse hyperbolic sine |
|
|
Compute inverse hyperbolic cosine |
|
|
Compute inverse hyperbolic tangent |
|
Mathematical Functions Example
import riemann as rm
# Create example tensor
x = rm.tensor([-2.5, 0.0, 1.5, 3.0])
print("Original tensor:", x)
# Basic mathematical functions
print("\n1. Basic mathematical functions:")
print("Absolute value:", rm.abs(x))
print("Square root:", rm.sqrt(rm.abs(x)))
print("Square:", rm.square(x))
print("Exponential:", rm.exp(x))
print("Natural logarithm:", rm.log(rm.abs(x) + 1e-10))
# Rounding functions
print("\n2. Rounding functions:")
print("Ceiling:", rm.ceil(x))
print("Floor:", rm.floor(x))
print("Round:", rm.round(x))
print("Truncate:", rm.trunc(x))
# Trigonometric functions
print("\n3. Trigonometric functions:")
angles = rm.tensor([0, rm.pi/4, rm.pi/2, rm.pi])
print("Angles:", angles)
print("Sine:", rm.sin(angles))
print("Cosine:", rm.cos(angles))
print("Tangent:", rm.tan(angles))
# Hyperbolic functions
print("\n4. Hyperbolic functions:")
print("Hyperbolic sine:", rm.sinh(x))
print("Hyperbolic cosine:", rm.cosh(x))
print("Hyperbolic tangent:", rm.tanh(x))
Statistical Functions
Riemann provides various statistical functions for tensor analysis. Here are the commonly used ones:
Common Statistical Functions
Function |
Description |
Example |
|---|---|---|
|
Compute sum of tensor elements |
|
|
Compute sum of multiple tensors |
|
|
Compute mean of tensor elements |
|
|
Compute variance of tensor elements |
|
|
Compute standard deviation of tensor elements |
|
|
Compute norm of tensor |
|
|
Compute maximum value of tensor elements |
|
|
Compute minimum value of tensor elements |
|
|
Compute element-wise maximum of two tensors |
|
|
Compute element-wise minimum of two tensors |
|
|
Select elements based on condition |
|
|
Clamp tensor values to a range |
|
|
Sort tensor elements |
|
|
Return indices that sort tensor |
|
|
Return index of maximum value |
|
|
Return index of minimum value |
|
|
Compute product of tensor elements |
|
|
Compute dot product of two tensors |
|
Statistical Functions Example
import riemann as rm
# Create example tensor
x = rm.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
y = rm.tensor([[2.0, 1.0, 4.0], [3.0, 6.0, 5.0], [8.0, 7.0, 10.0]])
z = rm.tensor([[1.0, 3.0, 2.0], [4.0, 2.0, 1.0], [3.0, 4.0, 5.0]])
print("Original tensor x:", x)
print("Original tensor y:", y)
print("Original tensor z:", z)
# 1. sum function
print("\n1. sum function:")
print("Sum of all elements:", rm.sum(x))
print("Sum along axis 0:", rm.sum(x, dim=0))
print("Sum along axis 1:", rm.sum(x, dim=1))
# 2. sumall function
print("\n2. sumall function:")
print("Sum of multiple tensors:", rm.sumall(x, y, z))
# 3. mean function
print("\n3. mean function:")
print("Mean of all elements:", rm.mean(x))
print("Mean along axis 0:", rm.mean(x, dim=0))
# 4. max and min functions
print("\n4. max and min functions:")
print("Maximum value:", rm.max(x))
print("Minimum value:", rm.min(x))
print("Maximum along axis 0:", rm.max(x, dim=0))
print("Minimum along axis 1:", rm.min(x, dim=1))
# 5. where function
print("\n5. where function:")
condition = x > 5
result = rm.where(condition, x, y)
print("Condition (x > 5):", condition)
print("Where result:", result)
where Function Detailed Example
The where function has two main use cases: 1. When only condition is provided, returns indices of elements that satisfy the condition 2. When condition, x, and y are provided, selects elements from x and y based on the condition
import riemann as rm
# Create example tensors
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
y = rm.tensor([[10, 20, 30], [40, 50, 60], [70, 80, 90]])
print("Original tensor x:")
print(x)
print("Original tensor y:")
print(y)
# Use case 1: Only provide condition, return indices of elements that satisfy the condition
print("\nUse case 1: Only provide condition")
condition = x > 5
indices = rm.where(condition)
print("Condition (x > 5):")
print(condition)
print("Indices of elements that satisfy the condition:")
print("Row indices:", indices[0])
print("Column indices:", indices[1])
print("Index tuple:", indices)
# Use case 2: Provide condition, x, and y, select elements based on condition
print("\nUse case 2: Provide condition, x, and y")
# Basic usage
result1 = rm.where(condition, x, y)
print("Basic usage result (take x where x > 5, otherwise take y):")
print(result1)
# Use scalar as x or y
result2 = rm.where(condition, 100, y)
print("\nResult with scalar as x (take 100 where x > 5, otherwise take y):")
print(result2)
result3 = rm.where(condition, x, 0)
print("\nResult with scalar as y (take x where x > 5, otherwise take 0):")
print(result3)
# Use tensors of different shapes (broadcasting will be applied)
print("\nUsing tensors of different shapes")
condition_1d = rm.tensor([True, False, True]) # 1D condition
x_1d = rm.tensor([100, 200, 300]) # 1D x
result4 = rm.where(condition_1d, x_1d, y)
print("Result with 1D condition and 1D x vs 2D y:")
print(result4)
# where function with gradient tracking
print("\nwhere function with gradient tracking")
x_grad = rm.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], requires_grad=True)
y_grad = rm.tensor([[10.0, 20.0, 30.0], [40.0, 50.0, 60.0]], requires_grad=True)
condition_grad = x_grad > 3.0
result_grad = rm.where(condition_grad, x_grad, y_grad)
print("Result with gradient tracking:")
print(result_grad)
# Compute gradients
sum_result = rm.sum(result_grad)
sum_result.backward()
print("\nGradient computation result:")
print("Gradient of x_grad:")
print(x_grad.grad)
print("Gradient of y_grad:")
print(y_grad.grad)
sumall Function Efficiency Advantage
The sumall function is more efficient than using tensor addition operations, especially in gradient tracking, because:
Reduced Computation Graph: When using sumall, the computation graph is reduced to a single layer, regardless of the number of tensors being summed.
Scalable Efficiency: With tensor addition operators (+), the computation graph grows linearly with each additional tensor, leading to increased graph complexity.
Faster Gradient Tracking: The simpler graph structure of sumall results in much faster gradient computation during backpropagation, especially when summing many tensors.
Gradient Tracking Efficiency Example
import riemann as rm
# Create tensors with gradient tracking
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = rm.tensor([4.0, 5.0, 6.0], requires_grad=True)
z = rm.tensor([7.0, 8.0, 9.0], requires_grad=True)
w = rm.tensor([10.0, 11.0, 12.0], requires_grad=True)
# Using sumall (more efficient)
print("\nUsing sumall:")
result_sumall = rm.sumall(x, y, z, w)
result_sumall.backward()
print("x.grad:", x.grad)
print("y.grad:", y.grad)
print("z.grad:", z.grad)
print("w.grad:", w.grad)
# Reset gradients
x.grad = None
y.grad = None
z.grad = None
w.grad = None
# Using addition operators (less efficient)
print("\nUsing addition operators:")
result_addition = x + y + z + w
result_addition.backward()
print("x.grad:", x.grad)
print("y.grad:", y.grad)
print("z.grad:", z.grad)
print("w.grad:", w.grad)
Other Statistical Functions Example
import riemann as rm
# Create example tensor
x = rm.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
y = rm.tensor([[2.0, 1.0, 4.0], [3.0, 6.0, 5.0], [8.0, 7.0, 10.0]])
print("Original tensor x:", x)
print("Original tensor y:", y)
# 1. clamp function
print("\n1. clamp function:")
clamped = rm.clamp(x, min=3.0, max=7.0)
print("Clamped between 3 and 7:", clamped)
# 2. argmax function
print("\n2. argmax function:")
print("Index of maximum value:", rm.argmax(x))
print("Indices of maximum along axis 0:", rm.argmax(x, dim=0))
# 3. maximum function
print("\n3. maximum function:")
max_result = rm.maximum(x, y)
print("Element-wise maximum of x and y:", max_result)
# 4. sort and argsort functions
print("\n4. sort and argsort functions:")
sorted_x, indices = rm.sort(x, dim=1, return_indices=True)
print("Sorted along axis 1:", sorted_x)
print("Sort indices:", indices)
argsorted = rm.argsort(x, dim=1)
print("Argsort along axis 1:", argsorted)
Statistical Functions with Gradient Tracking
import riemann as rm
# Create tensor with gradient tracking
x = rm.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], requires_grad=True)
print("Original tensor with grad:", x)
# 1. sum with gradient tracking
print("\n1. sum with gradient tracking:")
sum_result = rm.sum(x)
print("Sum result:", sum_result)
sum_result.backward()
print("Gradient of sum:", x.grad)
# Reset gradients
x.grad = None
# 2. mean with gradient tracking
print("\n2. mean with gradient tracking:")
mean_result = rm.mean(x)
print("Mean result:", mean_result)
mean_result.backward()
print("Gradient of mean:", x.grad)
# Reset gradients
x.grad = None
# 3. max with gradient tracking
print("\n3. max with gradient tracking:")
max_result = rm.max(x)
print("Max result:", max_result)
max_result.backward()
print("Gradient of max:", x.grad)
Tensor Comparison Operators
Riemann supports various tensor comparison operators for comparing tensor elements.
Operator |
Description |
Example |
Result Type |
|---|---|---|---|
|
Equal |
|
Boolean tensor |
|
Not equal |
|
Boolean tensor |
|
Less than |
|
Boolean tensor |
|
Less than or equal |
|
Boolean tensor |
|
Greater than |
|
Boolean tensor |
|
Greater than or equal |
|
Boolean tensor |
Comparison Operators Example
import riemann as rm
# Create example tensors
x = rm.tensor([1, 2, 3, 4])
y = rm.tensor([2, 2, 2, 2])
print("x:", x)
print("y:", y)
# Comparison operations
print("\nComparison results:")
print("x == y:", x == y)
print("x != y:", x != y)
print("x < y:", x < y)
print("x <= y:", x <= y)
print("x > y:", x > y)
print("x >= y:", x >= y)
Tensor Logical Operators
Riemann supports various tensor logical operators for logical operations on boolean tensors.
Operator |
Description |
Example |
Result Type |
|---|---|---|---|
|
Logical AND |
|
Boolean tensor |
|
Logical OR |
|
Boolean tensor |
|
Logical XOR |
|
Boolean tensor |
|
Logical NOT |
|
Boolean tensor |
Logical Operators Example
import riemann as rm
# Create boolean tensors
x = rm.tensor([True, True, False, False])
y = rm.tensor([True, False, True, False])
print("x:", x)
print("y:", y)
# Logical operations
print("\nLogical operation results:")
print("x & y:", x & y)
print("x | y:", x | y)
print("x ^ y:", x ^ y)
print("~x:", ~x)
Tensor Bitwise Operators
Riemann supports various tensor bitwise operators for bitwise operations on integer tensors.
Operator |
Description |
Example |
Result Type |
|---|---|---|---|
|
Bitwise AND |
|
Integer tensor |
|
Bitwise OR |
|
Integer tensor |
|
Bitwise XOR |
|
Integer tensor |
|
Bitwise NOT |
|
Integer tensor |
|
Left shift |
|
Integer tensor |
|
Right shift |
|
Integer tensor |
Bitwise Operators Example
import riemann as rm
# Create integer tensors
x = rm.tensor([1, 3, 5, 7], dtype=rm.int32)
y = rm.tensor([1, 2, 3, 4], dtype=rm.int32)
print("x:", x)
print("y:", y)
# Bitwise operations
print("\nBitwise operation results:")
print("x & y:", x & y)
print("x | y:", x | y)
print("x ^ y:", x ^ y)
print("~x:", ~x)
print("x << 1:", x << 1)
print("x >> 1:", x >> 1)
Tensor Check and Comparison Functions
Riemann provides various tensor check and comparison functions for checking tensor properties or comparing multiple tensors.
Function |
Description |
Example |
|---|---|---|
|
Check if all elements are true |
|
|
Check if any element is true |
|
|
Check if two tensors are equal within tolerance |
|
|
Check if two tensors are element-wise equal |
|
|
Check if two tensors are element-wise not equal |
|
|
Return indices of non-zero elements |
|
|
Return unique elements in tensor |
|
Check and Comparison Functions Example
import riemann as rm
# Create example tensors
x = rm.tensor([True, True, True])
y = rm.tensor([True, False, True])
z = rm.tensor([1.0, 2.0, 3.0])
w = rm.tensor([1.0, 2.0000001, 3.0])
print("x:", x)
print("y:", y)
print("z:", z)
print("w:", w)
# Check functions
print("\n1. Check functions:")
print("all(x):", rm.all(x))
print("any(y):", rm.any(y))
# Comparison functions
print("\n2. Comparison functions:")
print("equal(z, w):", rm.equal(z, w))
print("not_equal(z, w):", rm.not_equal(z, w))
print("allclose(z, w):", rm.allclose(z, w))
print("allclose(z, w, rtol=1e-03):", rm.allclose(z, w, rtol=1e-03))
Shape and Dimension Manipulation Functions
The following table lists all shape and dimension manipulation functions supported by Riemann:
Function |
Description |
Example |
|---|---|---|
|
Changes tensor shape without modifying data, supports -1 for auto-inference |
|
|
Alias for reshape, returns a view with the same data but different shape |
|
|
Flattens a range of tensor dimensions into one dimension |
|
|
Removes dimensions of size 1 |
|
|
Adds a dimension of size 1 at the specified position |
|
|
Expands tensor to specified shape, can only expand dimensions of size 1 |
|
|
Expands tensor to the same shape as another tensor |
|
|
Repeats tensor elements along specified dimensions |
|
|
Swaps two specified dimensions of the tensor |
|
|
Rearranges tensor dimensions according to specified order |
|
|
Flips tensor along specified dimensions |
|
|
Tensor transpose property, reverses entire dimension order for high-dimensional tensors |
|
|
Matrix transpose property, swaps only the last two dimensions |
|
|
Tensor conjugate transpose property |
|
|
Matrix conjugate transpose property, conjugate transpose of last two dimensions |
|
|
Concatenates tensor sequence along specified dimension |
|
|
Stacks tensor sequence along a new dimension |
|
|
Vertically stacks tensors, 1D tensors as rows, multi-dimensional along axis 0 |
|
|
Horizontally stacks tensors, 1D tensors concatenated, multi-dimensional along axis 1 |
|
|
Splits tensor into chunks along specified dimension (by chunk size) |
|
|
Splits tensor into chunks along specified dimension (by number of sections or indices) |
|
|
Vertically splits tensor (along dimension 0), splits tensor into multiple subtensors |
|
|
Horizontally splits tensor (along dimension 1), splits tensor into multiple subtensors |
|
|
Depth splits tensor (along dimension 2), splits 3D+ tensor into multiple subtensors |
|
|
Depth stacks tensors along dimension 2 (1D/2D tensors will be reshaped first) |
|
Tensor Type Conversion
Riemann provides various functions for tensor type conversion, including data type conversion and device switching.
Data Type Conversion Functions
Function |
Description |
Example |
|---|---|---|
|
Convert tensor to specified data type |
|
|
Convert tensor to the same data type as another tensor |
|
|
General conversion function, can convert data type and device |
|
|
Convert tensor to boolean type |
|
|
Convert tensor to float32 type |
|
|
Convert tensor to float64 type |
|
|
Return real part of complex tensor |
|
|
Return imaginary part of complex tensor |
|
|
Return complex conjugate of complex tensor |
|
Device Switching Functions
Function |
Description |
Example |
|---|---|---|
|
Move tensor to CUDA device |
|
|
Move tensor to CPU device |
|
|
General device switching function |
|
to() Function Detailed Parameters
Parameter |
Type |
Description |
Default |
|---|---|---|---|
|
TN |
Another tensor, use its device and dtype as target for migration |
None |
|
str or Device |
Target device, can be a string (e.g. ‘cpu’, ‘cuda’) or Device object |
None |
|
dtype |
Target data type, can be Python type, NumPy dtype, string or Riemann dtype |
None |
|
bool |
If True and data is in pinned memory, copying to GPU can be asynchronous with host computation. Only applicable for CPU -> GPU transfers |
False |
|
bool |
If True, always return a copy, even if device and dtype are the same |
False |
to() Function Usage Examples
import riemann as rm
# Convert data type
x = rm.tensor([1.0, 2.0, 3.0], dtype=rm.float32)
y = x.to(rm.float64)
print(f"Converted dtype: {y.dtype}")
# Convert device
x = rm.tensor([1.0, 2.0, 3.0], device='cpu')
y = x.to('cuda')
print(f"Converted device: {y.device}")
# Convert both dtype and device
x = rm.tensor([1.0, 2.0, 3.0], dtype=rm.float32, device='cpu')
y = x.to(rm.float64, device='cuda')
print(f"Converted dtype: {y.dtype}, device: {y.device}")
# Use keyword arguments
x = rm.tensor([1.0, 2.0, 3.0])
y = x.to(dtype=rm.float64, device='cuda')
# Copy dtype and device from another tensor
x = rm.tensor([1.0, 2.0, 3.0], dtype=rm.float64, device='cuda')
y = rm.tensor([4.0, 5.0, 6.0])
z = y.to(x)
print(f"Copied from x: dtype={z.dtype}, device={z.device}")
# Force copy
y = x.to(copy=True)
non_blocking Parameter Usage Example
import riemann as rm
# Create tensor on CPU
x = rm.tensor([1.0, 2.0, 3.0], device='cpu')
# Asynchronous transfer to GPU
# Note: Asynchronous transfer requires data to be in pinned memory
# In practice, it's recommended to synchronize device after transfer to ensure completion
y = x.to('cuda', non_blocking=True)
# Perform some CPU computations
# These can run in parallel with data transfer
cpu_result = x * 2
# Synchronize device to ensure data transfer is complete
# Must synchronize before accessing the GPU tensor
rm.cuda.synchronize()
# Now it's safe to use the GPU tensor
gpu_result = y * 2
Type Conversion Examples
import riemann as rm
# Create an integer tensor
x = rm.tensor([1, 2, 3], dtype=rm.int32)
print("Original tensor:", x)
print("Original data type:", x.dtype)
print("Original device:", x.device)
# 1. Data type conversion
print("\n1. Data type conversion:")
x_float = x.float()
print("Convert to float32:", x_float)
print("Data type:", x_float.dtype)
x_double = x.double()
print("\nConvert to float64:", x_double)
print("Data type:", x_double.dtype)
x_bool = x.bool()
print("\nConvert to bool:", x_bool)
print("Data type:", x_bool.dtype)
# 2. Using to function for conversion
print("\n2. Using to function for conversion:")
x_to_float = x.to(rm.float32)
print("Using to convert to float32:", x_to_float.dtype)
# 3. Complex number related conversions
print("\n3. Complex number related conversions:")
z = rm.tensor([1+2j, 3+4j], dtype=rm.complex64)
print("Complex tensor:", z)
print("Real part:", z.real())
print("Imaginary part:", z.imag())
print("Conjugate:", z.conj())
# 4. Device switching (if CUDA is available)
print("\n4. Device switching:")
if rm.cuda.is_available():
x_cuda = x.cuda()
print("Move to CUDA device:", x_cuda.device)
x_back_to_cpu = x_cuda.cpu()
print("Move back to CPU device:", x_back_to_cpu.device)
else:
print("CUDA not available, skipping device switching example")
Notes on Type Conversion
Data Type Conversion:
Converting from higher precision to lower precision types may cause precision loss
Converting from integer to floating point types is safe
Converting from floating point to integer types will truncate the decimal part
Device Switching:
Device switching creates a new tensor copy, consuming memory and time
Ensure the target device is available before switching
Tensors on different devices cannot be directly operated on, they need to be unified first
Complex Number Conversion:
real()andimag()functions return the real and imaginary parts of complex tensors, resulting in floating point typesconj()function returns the complex conjugate of complex tensors, resulting in complex type
Gradient Tracking
Enabling Gradient Tracking
To enable automatic differentiation, set requires_grad=True when creating a tensor:
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2
z = y.sum()
# Compute gradients
z.backward()
print(x.grad) # tensor([2., 2., 2.])
Disabling Gradient Tracking
You can disable gradient tracking for performance when you don’t need gradients:
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
# Method 1: Using no_grad context
with rm.no_grad():
y = x * 2 # No gradient tracking for this operation
# Method 2: Using requires_grad_
x.requires_grad_(False)
y = x * 2 # No gradient tracking
Gradient Context Management
Riemann provides various gradient context management tools to control gradient tracking behavior within specific code blocks. These tools can be used with with statements or decorators.
Using with Statements for Gradient Context Control
import riemann as rm
# Create tensor with gradient tracking
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
# 1. Using no_grad() to disable gradient tracking
print("\n1. Using no_grad() to disable gradient tracking:")
with rm.no_grad():
y = x * 2
print("y.requires_grad:", y.requires_grad) # False
# 2. Using enable_grad() to enable gradient tracking
print("\n2. Using enable_grad() to enable gradient tracking:")
with rm.no_grad():
# In this context, gradient tracking is disabled by default
z = x + 1
print("z.requires_grad:", z.requires_grad) # False
# But we can enable gradient tracking internally
with rm.enable_grad():
w = x * 3
print("w.requires_grad:", w.requires_grad) # True
# 3. Using set_grad_enabled() to manually set gradient tracking state
print("\n3. Using set_grad_enabled() to manually set gradient tracking state:")
with rm.set_grad_enabled(True):
a = x * 4
print("a.requires_grad:", a.requires_grad) # True
with rm.set_grad_enabled(False):
b = x * 5
print("b.requires_grad:", b.requires_grad) # False
Using Decorators for Gradient Context Control
In addition to with statements, Riemann also provides gradient context management tools in the form of decorators, which control gradient tracking behavior for entire functions.
import riemann as rm
# Create tensor with gradient tracking
x = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
# Using @no_grad decorator to disable gradient tracking in the function
@rm.no_grad
def inference_fn(tensor):
"""Inference function, no gradient tracking needed"""
result = tensor * 2 + 1
print("inference_fn: result.requires_grad =", result.requires_grad)
return result
# Using @enable_grad decorator to enable gradient tracking in the function
@rm.enable_grad
def training_fn(tensor):
"""Training function, gradient tracking needed"""
result = tensor * 3 + 2
print("training_fn: result.requires_grad =", result.requires_grad)
return result
# Test decorator effects
print("\nTesting @no_grad decorator:")
output1 = inference_fn(x)
print("\nTesting @enable_grad decorator:")
output2 = training_fn(x)
Application Scenarios for Gradient Context Management
Inference Phase: Disable gradient tracking during model inference to improve performance and save memory
Partial Computation: Only enable gradient tracking for the parts that need it in complex calculations
Nested Contexts: Flexibly switch gradient tracking states at different code levels
Function-Level Control: Set a unified gradient tracking strategy for entire functions through decorators
Indexing Operations
Riemann supports various tensor indexing operations for accessing array elements or slices. Here are the common indexing methods:
1. Integer Indexing
Integer indexing is used to access a single element at a specific position in the tensor. For multi-dimensional tensors, multiple integer indices can be used separated by commas.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Integer indexing
print("x[0, 0]:", x[0, 0]) # Get element at first row, first column
print("x[1, 2]:", x[1, 2]) # Get element at second row, third column
2. Negative Integer Indexing
Negative integer indexing counts from the end of the tensor, where -1 represents the last element, -2 represents the second last element, and so on.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Negative integer indexing
print("x[-1, -1]:", x[-1, -1]) # Get element at last row, last column
print("x[-2, -3]:", x[-2, -3]) # Get element at second last row, third last column
3. Slice Indexing
Slice indexing is used to access contiguous segments of the tensor, using colons (:) to represent ranges. The format is start:end:step, where start is the starting index, end is the ending index (exclusive), and step is the step size.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Slice indexing
print("x[:, 0]:", x[:, 0]) # Get first column of all rows
print("x[0, :]:", x[0, :]) # Get all columns of first row
print("x[1:, 1:]:", x[1:, 1:]) # Get sub-tensor starting from second row and second column
print("x[::2, ::2]:", x[::2, ::2]) # Get elements at every other row and column
4. Integer Array Indexing
Integer array indexing is used to access elements at positions specified by an integer array, returning a tensor with the same shape as the index array.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Integer array indexing
indices = rm.tensor([0, 1, 2])
print("x[indices, indices]:", x[indices, indices]) # Get diagonal elements
5. Boolean Indexing
Boolean indexing is used to access elements that satisfy conditions specified by a boolean array, returning a 1D tensor of elements that meet the conditions.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Boolean indexing
mask = x > 5
print("mask:", mask)
print("x[mask]:", x[mask]) # Get elements greater than 5
6. Mixed Indexing
Mixed indexing refers to using multiple indexing methods in the same indexing expression, such as using both integer indexing and slice indexing.
import riemann as rm
# Create tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("x:", x)
# Mixed indexing
print("x[0, 1:]:", x[0, 1:]) # Get elements from second column onwards in first row
print("x[1:, 0]:", x[1:, 0]) # Get elements from first column in second row and onwards
7. Indexing-Related Functions
Riemann provides several indexing-related functions for gathering or scattering data by indices:
Function |
Description |
Example |
|---|---|---|
|
Gather data by indices |
|
|
Scatter data by indices (non-in-place) |
|
|
Scatter data by indices (in-place) |
|
|
Scatter and accumulate data by indices (non-in-place) |
|
|
Scatter and accumulate data by indices (in-place) |
|
|
Set values at specified indices (non-in-place) |
|
|
Set values at specified indices (in-place) |
|
|
Add values at specified indices (non-in-place) |
|
|
Add values at specified indices (in-place) |
|
|
Subtract values at specified indices (non-in-place) |
|
|
Subtract values at specified indices (in-place) |
|
|
Multiply values at specified indices (non-in-place) |
|
|
Multiply values at specified indices (in-place) |
|
|
Divide values at specified indices (non-in-place) |
|
|
Divide values at specified indices (in-place) |
|
|
Raise values at specified indices to power (non-in-place) |
|
|
Raise values at specified indices to power (in-place) |
|
gather Function Example
The gather function is used to gather data from the specified dimension of the input tensor by indices.
import riemann as rm
# Create input tensor
input = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("input:", input)
# Define indices
index = rm.tensor([[0, 1], [1, 2]])
print("index:", index)
# Gather data along dimension 0
output = input.gather(0, index)
print("gather along dim 0:", output)
# Gather data along dimension 1
output = input.gather(1, index)
print("gather along dim 1:", output)
scatter Function Example
The scatter function is used to scatter data from the source tensor to the specified dimension of the target tensor by indices.
import riemann as rm
# Create target tensor
input = rm.tensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
print("input:", input)
# Define indices and source tensor
index = rm.tensor([[0, 1], [1, 2]])
src = rm.tensor([[10, 20], [30, 40]])
print("index:", index)
print("src:", src)
# Scatter data along dimension 0 (non-in-place)
output = input.scatter(0, index, src)
print("scatter along dim 0:", output)
# Scatter data along dimension 1 (non-in-place)
output = input.scatter(1, index, src)
print("scatter along dim 1:", output)
scatter_ Function Example
scatter_ is the in-place version of scatter, which directly modifies the input tensor.
import riemann as rm
# Create target tensor
input = rm.tensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]])
print("input:", input)
# Define indices and source tensor
index = rm.tensor([[0, 1], [1, 2]])
src = rm.tensor([[10, 20], [30, 40]])
print("index:", index)
print("src:", src)
# Scatter data along dimension 1 (in-place)
input.scatter_(1, index, src)
print("after scatter_ along dim 1:", input)
scatter_add Function Example
The scatter_add function is used to scatter and accumulate data from the source tensor to the specified dimension of the target tensor by indices.
import riemann as rm
# Create target tensor
input = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("input:", input)
# Define indices and source tensor
index = rm.tensor([[0, 1], [1, 2]])
src = rm.tensor([[10, 20], [30, 40]])
print("index:", index)
print("src:", src)
# Scatter and accumulate data along dimension 1 (non-in-place)
output = input.scatter_add(1, index, src)
print("scatter_add along dim 1:", output)
scatter_add_ Function Example
scatter_add_ is the in-place version of scatter_add, which directly modifies the input tensor.
import riemann as rm
# Create target tensor
input = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("input:", input)
# Define indices and source tensor
index = rm.tensor([[0, 1], [1, 2]])
src = rm.tensor([[10, 20], [30, 40]])
print("index:", index)
print("src:", src)
# Scatter and accumulate data along dimension 1 (in-place)
input.scatter_add_(1, index, src)
print("after scatter_add_ along dim 1:", input)
setat and setat_ Function Examples
The setat function is used to set values at specified indices (non-in-place), while setat_ is the in-place version.
import riemann as rm
# Create input tensor
input = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("input:", input)
# 1. Using setat (non-in-place)
print("\n1. Using setat (non-in-place):")
# Set values at specified indices
indices = (0, 1) # Row 0, column 1
value = 99
output = input.setat(indices, value)
print("setat result:", output)
print("original input unchanged:", input)
# 2. Using setat_ (in-place)
print("\n2. Using setat_ (in-place):")
# Set values at specified indices
indices = (1, 2) # Row 1, column 2
value = 88
input.setat_(indices, value)
print("after setat_:", input)
# 3. Using setat with multiple indices
print("\n3. Using setat with multiple indices:")
input = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
indices = [[0, 0], [2, 2]] # (0,0) and (2,2)
value = 100
output = input.setat(indices, value)
print("setat with multiple indices:", output)
Indexing Operation Notes
Index Out of Bounds:Using indices beyond the tensor range will result in an error.
Memory Layout:Different indexing methods may affect the memory layout of the returned tensor. Some indexing operations may return a view of the original tensor instead of a copy.
Gradient Tracking:For tensors requiring gradient tracking, some indexing operations may affect gradient calculation, especially in-place operations.
Performance Considerations:For large tensors, integer array indexing and boolean indexing may be slower than slice indexing because they create new tensor copies.
gather/scatter Function Parameters:
dim:Specifies the dimension of the operationindex:Specifies the index positionssrc:Specifies the source data (only for scatter-related functions)For in-place operations (functions with underscores), ensure the input tensor is not a leaf node, otherwise an error will occur.
In-place Operations
In-place operations modify the tensor directly without creating a new tensor:
x = rm.tensor([1, 2, 3])
# In-place addition
x += 1 # Same as x.add_(1)
# In-place multiplication
x *= 2 # Same as x.mul_(2)
# In-place assignment
x[0] = 10
In-place Operations Functions and Operators
Riemann supports the following in-place operations functions and operators:
Function |
Description |
Equivalent Operator |
Example |
|---|---|---|---|
|
In-place addition |
|
|
|
In-place subtraction |
|
|
|
In-place multiplication |
|
|
|
In-place division |
|
|
|
In-place power operation |
|
|
|
In-place set all elements to 0 |
None |
|
|
In-place fill all elements with a specified value |
None |
|
|
In-place copy data from another tensor |
None |
|
|
In-place detach gradients, making tensor no longer track gradients |
None |
|
|
In-place fill values based on mask |
None |
|
|
In-place fill diagonal elements |
None |
|
|
In-place set value at specified position |
|
|
|
In-place perform addition at specified position |
|
|
|
In-place perform subtraction at specified position |
|
|
|
In-place perform multiplication at specified position |
|
|
|
In-place perform division at specified position |
|
|
|
In-place perform power operation at specified position |
|
|
|
In-place scatter values according to index |
None |
|
|
In-place scatter and accumulate values according to index |
None |
|
Notes on In-place Operations
When using in-place operations, please note the following:
Leaf Node Restrictions with Gradient Tracking
For leaf node tensors with
requires_grad=True, in-place operations are not allowedThis is because in-place operations modify tensor values, which may compromise the correctness of gradient calculation
Gradient Tracking for Right-hand Side Values
The gradient of the right-hand side value (such as
yinx += y) can be tracked normallyThis means that even when using in-place operations, the gradient calculation of the right-hand side tensor is not affected
Gradient Tracking for In-place Operation Objects
For non-leaf node tensors, the gradient tracking result of in-place operations is more complex
Especially when assigning values to arrays by index (such as
x[index] = val), gradient calculation may produce unexpected behaviorIt is recommended to use in-place operations with caution in scenarios where gradient tracking is required
Recommended Usage Scenarios
In-place operations can be used on newly created tensors without gradient tracking attributes ( requires_grad=False )
For objects after
clone()orcopy(), which are not leaf nodes withrequires_grad=True, so in-place operations can be usedIn the inference phase where gradient calculation is not needed, using in-place operations can save memory
Memory Optimization
In-place operations do not create new tensor objects, so they can save memory
When processing large tensors, appropriate use of in-place operations can significantly reduce memory usage
Chained Operations
In-place operations return self , so they can be chained
For example: x.add_(y).mul_(z) is valid, while (x + y) * z is a chain of non-in-place operations
Gradient Tracking Example for In-place Operations
Here is an example of gradient tracking for in-place array assignment by index:
import riemann as rm
# Create tensors with gradient tracking
x0 = rm.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = rm.tensor([10.0, 20.0, 30.0], requires_grad=True)
# Print original values
print("Original values:")
print("x0:", x0)
print("y:", y)
# Clone x to make it no longer a leaf node, allowing in-place operations
x = x0.clone()
print("\nAfter x.clone(), x is no longer a leaf node")
print("x.is_leaf:", x.is_leaf)
# Perform in-place assignment by index
print("\nPerforming in-place assignment x[1] = y[0]")
x[1] = y[0]
# Print values after assignment
print("\nAfter assignment:")
print("x0:", x0)
print("x:", x)
print("y:", y)
# Calculate loss function
loss = x.sum()
print("\nLoss value:", loss)
# Backward propagation to calculate gradients
loss.backward()
# Print gradients
print("\nGradient tracking results:")
print("x0.grad:", x0.grad) # Gradient in left-hand side direction
print("y.grad:", y.grad) # Gradient in right-hand side direction
Output Analysis:
After in-place assignment, the value of x becomes [1.0, 10.0, 3.0], while the value of y remains unchanged
Gradient calculation results show:
x0.grad is [1.0, 0.0, 1.0], indicating that gradients are normally tracked except at the in-place assignment position
y.grad is [1.0, 0.0, 0.0], indicating that gradients in the right-hand side direction are normally tracked
Conclusion:
Gradient tracking for the right-hand side is not affected by in-place operations and works normally
Gradient tracking for the left-hand side may exhibit abnormal behavior at the in-place assignment position
For leaf nodes with gradient tracking, you must clone() them before performing in-place operations
Therefore, in-place operations should be used with caution in scenarios requiring precise gradient calculation
Diagonalization Operations
Riemann provides various diagonalization operation functions for handling tensor diagonal elements, triangular parts, etc. Here are the commonly used diagonalization operation functions:
diagonal Function
Extracts diagonal elements from the input tensor.
import riemann as rm
# Create example tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("Original tensor:")
print(x)
# Extract main diagonal
print("\nMain diagonal:")
print(rm.diagonal(x)) # tensor([1, 5, 9])
# Extract offset diagonal
print("\nOffset diagonal (offset=1):")
print(rm.diagonal(x, offset=1)) # tensor([2, 6])
# Extract negative offset diagonal
print("\nNegative offset diagonal (offset=-1):")
print(rm.diagonal(x, offset=-1)) # tensor([4, 8])
diag Function
Extracts diagonal elements from a tensor or creates a diagonal matrix from a 1D tensor.
import riemann as rm
# Create diagonal matrix from 1D tensor
v = rm.tensor([1, 2, 3])
print("\nCreate diagonal matrix from 1D tensor:")
print(rm.diag(v))
# Output:
# tensor([[1, 0, 0],
# [0, 2, 0],
# [0, 0, 3]])
# Extract diagonal elements from 2D tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\nExtract diagonal elements from 2D tensor:")
print(rm.diag(x)) # tensor([1, 5, 9])
batch_diag Function
Generates batch diagonal matrices from batch 1D tensors.
import riemann as rm
# Create batch 1D tensor
batch_v = rm.tensor([[1, 2], [3, 4]])
print("\nBatch 1D tensor:")
print(batch_v)
# Generate batch diagonal matrices
print("\nBatch diagonal matrices:")
print(rm.batch_diag(batch_v))
# Output:
# tensor([[[1, 0],
# [0, 2]],
#
# [[3, 0],
# [0, 4]]])
fill_diagonal Function
Fills the diagonal elements between specified dimensions with a given value, returns a new tensor.
import riemann as rm
# Create example tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\nOriginal tensor:")
print(x)
# Fill main diagonal with 0
print("\nFill main diagonal with 0:")
print(rm.fill_diagonal(x, 0))
# Output:
# tensor([[0, 2, 3],
# [4, 0, 6],
# [7, 8, 0]])
# Fill offset diagonal with 5
print("\nFill offset diagonal with 5 (offset=1):")
print(rm.fill_diagonal(x, 5, offset=1))
fill_diagonal_ Function
In-place fills the diagonal elements between specified dimensions with a given value, returns the original tensor.
import riemann as rm
# Create example tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\nOriginal tensor:")
print(x)
# In-place fill main diagonal with 0
print("\nIn-place fill main diagonal with 0:")
result = rm.fill_diagonal_(x, 0)
print(result)
print("Is original tensor modified:")
print(x)
tril Function
Extracts the lower triangular part of the tensor (including the diagonal).
import riemann as rm
# Create example tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\nOriginal tensor:")
print(x)
# Extract lower triangular part
print("\nLower triangular part:")
print(rm.tril(x))
# Output:
# tensor([[1, 0, 0],
# [4, 5, 0],
# [7, 8, 9]])
# Extract offset lower triangular part
print("\nOffset lower triangular part (diagonal=-1):")
print(rm.tril(x, diagonal=-1))
triu Function
Extracts the upper triangular part of the tensor (including the diagonal).
import riemann as rm
# Create example tensor
x = rm.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("\nOriginal tensor:")
print(x)
# Extract upper triangular part
print("\nUpper triangular part:")
print(rm.triu(x))
# Output:
# tensor([[1, 2, 3],
# [0, 5, 6],
# [0, 0, 9]])
# Extract offset upper triangular part
print("\nOffset upper triangular part (diagonal=1):")
print(rm.triu(x, diagonal=1))
Function Parameter Description
Function Name |
Main Parameters |
Default Values |
Description |
|---|---|---|---|
|
input, offset, dim1, dim2 |
offset=0, dim1=0, dim2=1 |
Extracts diagonal elements between specified dimensions |
|
input, offset |
offset=0 |
Extracts diagonal elements or creates diagonal matrix |
|
v |
None |
Generates batch diagonal matrices from batch 1D tensors |
|
input, value, offset, dim1, dim2 |
offset=0, dim1=-2, dim2=-1 |
Fills diagonal elements, returns new tensor |
|
input, value, offset, dim1, dim2 |
offset=0, dim1=-2, dim2=-1 |
In-place fills diagonal elements, returns original tensor |
|
input_tensor, diagonal |
diagonal=0 |
Extracts lower triangular part |
|
input_tensor, diagonal |
diagonal=0 |
Extracts upper triangular part |
Notes
diagonalFunction:Input tensor must be at least 2-dimensional
dim1 and dim2 cannot be the same
Supports negative indices (-1 represents the last dimension)
diagFunction:When input is 1D tensor, returns diagonal matrix
When input is 2D tensor, returns diagonal elements
Does not support 3D or higher-dimensional inputs
batch_diagFunction:The last dimension of the input tensor is the length of the diagonal elements
The output tensor shape is
(*, n, n), where n is the size of the last dimension of the input tensor
fill_diagonalandfill_diagonal_Functions:input tensor must be at least 2-dimensional
dim1 and dim2 cannot be the same
Support negative indices (default fills the diagonal of the last two dimensions)
fill_diagonal_is an in-place operation that modifies the original tensor
trilandtriuFunctions:The diagonal parameter controls the offset of the diagonal
diagonal=0 represents the main diagonal
diagonal>0 represents above the main diagonal
diagonal<0 represents below the main diagonal
Saving and Loading Tensors
Riemann provides PyTorch-compatible serialization functionality for saving and loading tensors, parameters, module states, and training checkpoints. These features use ZIP format for serialization, ensuring cross-platform compatibility and efficient storage.
Basic Usage
Save and load a single tensor:
import riemann as rm
# Create tensor
x = rm.tensor([1, 2, 3])
# Save to file
rm.save(x, 'tensor.pt')
# Load from file
y = rm.load('tensor.pt')
print(y) # tensor([1, 2, 3])
Saving Multi-dimensional Tensors
You can save tensors of any shape and dimension:
# Create multi-dimensional tensors
matrix = rm.randn(3, 4)
tensor_3d = rm.randn(2, 3, 4)
# Save multi-dimensional tensors
rm.save(matrix, 'matrix.pt')
rm.save(tensor_3d, 'tensor_3d.pt')
# Load and verify
loaded_matrix = rm.load('matrix.pt')
loaded_tensor_3d = rm.load('tensor_3d.pt')
print(f"Matrix shape: {loaded_matrix.shape}") # (3, 4)
print(f"3D tensor shape: {loaded_tensor_3d.shape}") # (2, 3, 4)
Saving Model State Dict
When training deep learning models, you typically need to save the model’s parameter state:
# Create a simple neural network
model = rm.nn.Sequential(
rm.nn.Linear(10, 64),
rm.nn.ReLU(),
rm.nn.Linear(64, 10)
)
# Save model state dict
rm.save(model.state_dict(), 'model_weights.pt')
# Create new model and load weights
new_model = rm.nn.Sequential(
rm.nn.Linear(10, 64),
rm.nn.ReLU(),
rm.nn.Linear(64, 10)
)
new_model.load_state_dict(rm.load('model_weights.pt'))
Saving Training Checkpoints
During training, you can save complete checkpoints containing model state, optimizer state, and training progress:
# Assume model training is in progress
model = rm.nn.Linear(10, 5)
optimizer = rm.optim.Adam(model.parameters(), lr=0.001)
# Train for several epochs
for epoch in range(10):
# ... training code ...
pass
# Save complete training checkpoint
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': 0.5, # Current loss value
}
rm.save(checkpoint, 'checkpoint.pt')
# Resume training from checkpoint
checkpoint = rm.load('checkpoint.pt')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch'] + 1
loss = checkpoint['loss']
print(f"Resuming training from epoch {start_epoch}, last loss: {loss}")
Device Mapping for Loading
When loading models between different devices (CPU/GPU), you can use the map_location parameter to specify the loading location:
# Load tensor saved on GPU to CPU
# Assume training and saving on GPU
# rm.save(gpu_tensor, 'gpu_tensor.pt')
# Load on CPU
cpu_tensor = rm.load('gpu_tensor.pt', map_location='cpu')
# Use dictionary for device mapping
map_location = {'cuda:0': 'cpu', 'cuda:1': 'cpu'}
cpu_tensor = rm.load('model.pt', map_location=map_location)
Saving Multiple Tensors
You can save multiple tensors in a single file:
# Create multiple tensors
tensor_a = rm.randn(3, 3)
tensor_b = rm.randn(4, 4)
tensor_c = rm.tensor([1, 2, 3, 4, 5])
# Save as dictionary
tensor_dict = {
'weights': tensor_a,
'biases': tensor_b,
'labels': tensor_c
}
rm.save(tensor_dict, 'tensors.pt')
# Load and access individual tensors
loaded_dict = rm.load('tensors.pt')
weights = loaded_dict['weights']
biases = loaded_dict['biases']
labels = loaded_dict['labels']
Important Notes
File Format: Riemann uses ZIP format for serialization, with file extensions typically being
.ptor.pthCompatibility: The serialization format is compatible with PyTorch, and can load PyTorch-saved tensors (with some limitations)
Device Information: Saved tensors retain device information (CPU/GPU), which can be remapped using
map_locationduring loadingGradient Information: When saving tensors, gradient computation graph information (requires_grad attribute) is preserved
Large File Handling: For large models, it is recommended to use checkpoint mechanisms to save in chunks to avoid memory issues
Einstein Summation Convention (einsum)
The Einstein summation convention is a concise notation for tensor operations in mathematics and physics. Riemann’s einsum function leverages this convention to provide a unified, elegant, and powerful way to express various tensor operations.
Core Concept
The core rule of the Einstein summation convention is: when the same index appears twice in a term, it indicates summation over that index.
For example, matrix multiplication \(C_{ik} = \sum_{j} A_{ij} B_{jk}\) can be written as ij,jk->ik.
Basic Syntax
# Matrix multiplication
C = rm.einsum('ij,jk->ik', A, B)
# Batch matrix multiplication
C = rm.einsum('bij,bjk->bik', A, B)
# Using ellipsis to support arbitrary batch dimensions
C = rm.einsum('...ij,...jk->...ik', A, B)
# Matrix trace
trace = rm.einsum('ii->', A)
# Diagonal extraction
diag = rm.einsum('ii->i', A)
Operations einsum Can Replace
einsum can uniformly express various tensor operations:
Operation Type |
einsum Equation |
Description |
|---|---|---|
Matrix Multiplication |
|
Standard matrix multiplication |
Batch Matrix Multiplication |
|
Supports arbitrary batch dimensions |
Matrix Trace |
|
Sum of diagonal elements |
Diagonal Extraction |
|
Extract diagonal elements as vector |
Matrix Transpose |
|
Swap rows and columns |
Vector Dot Product |
|
Vector inner product |
Vector Outer Product |
|
Generate rank-1 matrix |
Hadamard Product |
|
Element-wise multiplication |
Frobenius Inner Product |
|
Matrix inner product |
Sum All Elements |
|
Sum of all elements |
Sum Along Rows/Columns |
|
Sum along specified dimension |
Multi-Operand Chain |
|
Sequential matrix multiplication |
Usage Examples
import riemann as rm
# Matrix multiplication
A = rm.tensor([[1, 2], [3, 4]])
B = rm.tensor([[5, 6], [7, 8]])
C = rm.einsum('ij,jk->ik', A, B)
# Batch matrix multiplication
batch_A = rm.randn(2, 3, 4)
batch_B = rm.randn(2, 4, 5)
batch_C = rm.einsum('bij,bjk->bik', batch_A, batch_B)
# Trace operation
trace = rm.einsum('ii->', A)
# Vector operations
a = rm.tensor([1, 2, 3])
b = rm.tensor([4, 5, 6])
dot = rm.einsum('i,i->', a, b) # Dot product
outer = rm.einsum('i,j->ij', a, b) # Outer product
Detailed Documentation
For complete documentation on einsum, including detailed syntax rules, all computation scenarios, and more examples, please refer to Einstein Summation Convention (einsum).