Hi,
Sorry for the late response from my side. @tom already gave a great answer. I just want to add that in many cases, you can convert using just .data<float>(), possibly preceded by a call to .contiguous() if you suspect the tensor is non-contiguous (like after slicing). For example, if you have a simple flat tensor:
at::Tensor my_tensor = torch::randn(5);
std::vector<float> v(my_tensor.data<float>(), my_tensor.data<float>() + my_tensor.numel());

For you second question, the error you get is very much expected. In fact, your C++ code and Python code have a subtle but important difference. Your python code is updating b.data, while your C++ code is updating the variable itself. This would give you the same error in Python:
b -= learning_rate * b.grad

which is what your C++ code is doing. So an easy fix is to write the following in C++:
b.data() -= lr * b.grad().data();

The difference, just like in Python, is that Variables that require gradients may not be updated.
However, both the Python and C++ version using .data() are not quite the most modern/idiomatic PyTorch. When you want to update a variable in place without recording a gradient for this (as, commonly, in optimizer code), you should instead temporarily disable gradient recording. In Python you would write this as:
with torch.no_grad():
    b -= learning_rate * b.grad

and in C++ as
NoGradGuard guard;
b -= lr * b.grad();

Notice that in both cases we need not use .data(), which is much better. Hope this helps.