Data Manipulation

using candle in Rust to show Tensor data manipulation

use candle_core::{Device, Tensor};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cpu = Device::Cpu;

    let g = Tensor::arange::<f32>(0., 12., &cpu)?;
    println!("cpu g = {g}");

    let g = g.reshape((3, 4))?;

    let gpu = Device::new_metal(0)?;
    let x = Tensor::arange::<f32>(0., 12., &gpu)?;
    println!("metal x = {x}");

    println!("x element count = {}", x.elem_count());

    println!("x shape = {:?}", x.shape());

    let x = x.reshape((3, 4))?;

    println!("x after reshape is\n{}, shape is {:?}", x, x.shape());

    let zeros_tensor = Tensor::zeros((2, 3, 4), candle_core::DType::F32, &cpu)?;
    println!("tensor zeros:\n{}", zeros_tensor);

    println!(
        "tensor ones:\n{}",
        Tensor::ones((2, 3, 4), candle_core::DType::F32, &cpu)?
    );

    println!("tensor random:\n{}", Tensor::randn(0.0, 1.0, (3, 4), &cpu)?);

    println!(
        "tensor specified:\n{}",
        Tensor::new(&[[2_i64, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]], &cpu)?
    );

    println!("x[-1] = {:?}", x.get(2)?.to_vec1::<f32>()?);
    println!(
        "x[1:3] = {:?}",
        x.index_select(&Tensor::new(&[1_i64, 2], &gpu)?, 0)?
            .to_vec2::<f32>()?
    );

    x.get(1)?.slice_set(&Tensor::new(&[17_f32], &gpu)?, 0, 2)?;
    println!("x = \n{}", x);

    let y = Tensor::from_slice(&[12_f32; 8], (2, 4), &gpu)?;
    let x = x.slice_assign(&[0..2, 0..4], &y)?;
    println!("x = \n{}", x);

    let z = x.to_device(&cpu)?;
    println!("x exp = \n{}", x.exp()?);
    println!("z exp = \n{}", z.exp()?);

    let p = Tensor::from_slice(&[1_f32, 2., 4., 8.], (1, 4), &gpu)?;
    let q = Tensor::from_slice(&[2_f32; 4], (1, 4), &gpu)?;

    println!("p = {p},\nq = {q}");

    println!("p + q = {}", (p.clone() + q.clone())?);
    println!("p - q = {}", (p.clone() - q.clone())?);
    println!("p * q = {}", (p.clone() * q.clone())?);
    println!("p / q = {}", (p.clone() / q.clone())?);
    println!("p ** q = {}", (p.clone().pow(&q))?);

    let gz0 = Tensor::cat(&[g.clone(), z.clone()], 0)?;
    let gz1 = Tensor::cat(&[g.clone(), z.clone()], 1)?;

    println!("gz0 = \n{gz0}");
    println!("gz1 = \n{gz1}");

    println!("z == g:\n{}", z.eq(&g)?);

    println!("z < g:\n{}", z.lt(&g)?);

    println!("g sum = {}", g.sum_all()?);

    let a = Tensor::arange(0_i64, 3, &gpu)?.reshape((3, 1))?;
    println!("a = \n{a}");

    let b = Tensor::arange(0_i64, 2, &gpu)?.reshape((1, 2))?;
    println!("b = \n{b}");

    println!("a + b = \n{}", a.broadcast_add(&b)?);

    Ok(())
}

I’m having a hard time trying to wrap my head around how broadcasting works. Could anyone confirm that my understanding is correct?

For each corresponding dimension (from the trailing) of two (or more) tensors, one of them must be either one or zero or they must have the size. Otherwise, it won’t work.

m = torch.arange(12).reshape((6, 1, 2))
n = torch.arange(6).reshape((3, 2))
# -1th -> 2 and 2 (same)
# -2th -> 1 and 3 (one of them is 1)
# -3th -> 6 and 0 (one of them is 0)

Then one of the corresponding dimension is expanded to match each other.

Q1 . Run the code in this section. Change the conditional statement X == Y to X < Y or X >Y, and then see what kind of tensor you can get. ( Ex. 2.1.8)

X = torch.arange(2, 25, 2, dtype = torch.float32).reshape(3,4)
Y = torch.tensor([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
X,Y

(tensor([[ 2., 4., 6., 8.],
[10., 12., 14., 16.],
[18., 20., 22., 24.]]),
tensor([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]]))

X==Y, X<Y, X>Y
(tensor([[False, False, False, False],
[False, False, False, False],
[False, False, False, False]]),
tensor([[False, False, False, False],
[False, False, False, False],
[False, False, False, False]]),
tensor([[True, True, True, True],
[True, True, True, True],
[True, True, True, True]]))

Q2. Replace the two tensors that operate by element in the broadcasting mechanism with other shapes, e.g., 3-dimensional tensors. Is the result the same as expected?

p = torch.arange(1, 25, 2, dtype = torch.float32).reshape(3,2,2)
q = torch.arange(5, 17, dtype=torch.float32).reshape(2,3,2)
p,q

(tensor([[[ 1., 3.],
[ 5., 7.]],

     [[ 9., 11.],
      [13., 15.]],

     [[17., 19.],
      [21., 23.]]]),

tensor([[[ 5., 6.],
[ 7., 8.],
[ 9., 10.]],

     [[11., 12.],
      [13., 14.],
      [15., 16.]]]))

p+q


RuntimeError Traceback (most recent call last)
Cell In[14], line 1
----> 1 p+q

RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1

p = torch.arange(1, 25, 2, dtype = torch.float32).reshape(3,1,4)
q = torch.arange(5, 17, dtype=torch.float32).reshape(3,4,1)
p,q

(tensor([[[ 1., 3., 5., 7.]],

     [[ 9., 11., 13., 15.]],

     [[17., 19., 21., 23.]]]),

tensor([[[ 5.],
[ 6.],
[ 7.],
[ 8.]],

     [[ 9.],
      [10.],
      [11.],
      [12.]],

     [[13.],
      [14.],
      [15.],
      [16.]]]))

p+q

tensor([[[ 6., 8., 10., 12.],
[ 7., 9., 11., 13.],
[ 8., 10., 12., 14.],
[ 9., 11., 13., 15.]],

    [[18., 20., 22., 24.],
     [19., 21., 23., 25.],
     [20., 22., 24., 26.],
     [21., 23., 25., 27.]],

    [[30., 32., 34., 36.],
     [31., 33., 35., 37.],
     [32., 34., 36., 38.],
     [33., 35., 37., 39.]]])

P.S it requires at least one of the dimensions (either 1 or 2 - not 0) to be singleton. Only then it is able to broadcast and stretch

@dscyrescotti I will try to explain. I hope the internal working becomes more clear after this.

Lets divide this whole process in steps.
Step 1 you create two tensors m = (6,1,2) and n = (3,2)
Step 2 when you perform operation on these two tensors, they are first rearranged as (6,1,2)
(3,2)
given the code both of tensor shapes are read from the right (as you mentioned) but the missing dimension in n doesnt become 0, it becomes 1.
Step 3 both tensors are expanded in a shape (6,3,2) - I’d suggest try solving it on a paper. You’ll get a clearer picture.
Step 4 now since both the tensors have same shape performing addition operation is possible!

m = torch.arange(1,13).reshape(6,1,2)
m

tensor([[[ 1, 2]],

    [[ 3,  4]],

    [[ 5,  6]],

    [[ 7,  8]],

    [[ 9, 10]],

    [[11, 12]]])
n = torch.arange(1,7).reshape(3,2)
n

tensor([[1, 2],
[3, 4],
[5, 6]])

m+n

tensor([[[ 2, 4],
[ 4, 6],
[ 6, 8]],

    [[ 4,  6],
     [ 6,  8],
     [ 8, 10]],

    [[ 6,  8],
     [ 8, 10],
     [10, 12]],

    [[ 8, 10],
     [10, 12],
     [12, 14]],

    [[10, 12],
     [12, 14],
     [14, 16]],

    [[12, 14],
     [14, 16],
     [16, 18]]])

Try to solve this by hand, using the same values and then compare. It will help a lot :slight_smile:

1 Like