- 2025-02-05
-
发表了主题帖:
《深度学习的数学——使用Python语言》11.深入理解梯度下降
本帖最后由 waterman 于 2025-2-5 12:41 编辑
梯度下降是机器学习和深度学习的核心优化算法,几乎所有的模型训练都离不开它。然而,梯度下降并不是一个单一的算法,而是一个庞大的家族,包含了许多变体和改进方法。本文将从最基础的梯度下降开始,逐步深入学习,并通过代码进一步理解每个算法的实现细节。
梯度下降的基本原理
梯度下降的核心思想是通过迭代的方式,沿着目标函数的负梯度方向逐步调整参数,最终找到函数的最小值。这个过程可以类比为一个盲人下山的过程:盲人无法看到整个山的地形,但可以通过脚下的坡度来判断下山的方向,并一步步向山脚移动。
一维梯度下降
让我们从一个简单的例子开始:一维函数 f(x)=x^2。这个函数的导数为 f′(x)=2x,因此梯度下降的更新规则为:
其中,η是学习率,控制着每一步的步长。以下是一个简单的 Python 实现:
def gradient_descent_1d(start, lr=0.1, epochs=100):
x = start
history = []
for _ in range(epochs):
grad = 2 * x # 计算梯度
x -= lr * grad # 更新参数
history.append(x)
return history
# 测试
start_value = 10.0
learning_rate = 0.1
result = gradient_descent_1d(start_value, lr=learning_rate, epochs=50)
print("最终结果:", result[-1])
在这个例子中,我们从初始值 x=10.0开始,经过 50 次迭代后,x的值会逐渐趋近于 0,即函数的最小值。
其中,学习率的选择对结果有重要影响,过大的学习率可能导致震荡,而过小的学习率则会导致收敛速度过慢。
二维梯度下降
接下来,我们考虑一个二维函数 f(x,y)=x^2+2y^2。这个函数的梯度为 ∇f=[2x,4y],因此梯度下降的更新规则为:
以下是二维梯度下降的 Python 实现:
def gradient_descent_2d(start, lr=0.1, epochs=100):
x, y = start
history = []
for _ in range(epochs):
grad_x = 2 * x # x 方向的梯度
grad_y = 4 * y # y 方向的梯度
x -= lr * grad_x # 更新 x
y -= lr * grad_y # 更新 y
history.append((x, y))
return history
# 测试
start_value = (5.0, 5.0)
learning_rate = 0.1
result = gradient_descent_2d(start_value, lr=learning_rate, epochs=50)
print("最终结果:", result[-1])
运行结果如下:
在这个例子中,我们可以看到梯度下降在不同维度上的收敛速度是不同的。由于y方向的梯度是x方向的两倍,因此在y方向上的收敛速度会更快。这种差异在某些情况下会导致优化路径呈现出椭圆形的轨迹。
随机梯度下降(SGD)
在实际的机器学习问题中,目标函数通常是基于大量数据的损失函数。传统的梯度下降需要计算整个数据集的梯度,这在数据量较大时会变得非常耗时。为了解决这个问题,随机梯度下降(Stochastic Gradient Descent, SGD)应运而生。
SGD 的基本原理
SGD 的核心思想是每次迭代只使用一个随机样本或一个小批量样本来估计梯度。虽然这种估计会引入噪声,但它在实践中通常能够显著加快收敛速度,尤其是在大规模数据集上。
以下是 SGD 的 Python 实现:
import numpy as np
def sgd(data, start, lr=0.1, epochs=100, batch_size=1):
x, y = start
history = []
n_samples = len(data)
for _ in range(epochs):
# 随机打乱数据
np.random.shuffle(data)
for i in range(0, n_samples, batch_size):
batch = data[i:i + batch_size]
grad_x = 2 * x # 假设梯度计算
grad_y = 4 * y # 假设梯度计算
x -= lr * grad_x # 更新 x
y -= lr * grad_y # 更新 y
history.append((x, y))
return history
# 测试
data = np.random.randn(100, 2) # 生成随机数据
start_value = (5.0, 5.0)
learning_rate = 0.1
result = sgd(data, start_value, lr=learning_rate, epochs=10, batch_size=10)
print("最终结果:", result[-1])
运行结果如下:
SGD 的优缺点
SGD 的主要优点是计算效率高,尤其是在大规模数据集上。然而,由于每次迭代只使用部分数据,SGD 的更新方向可能会引入较大的噪声,导致收敛路径不稳定。为了缓解这个问题,通常会采用学习率衰减策略,即在训练过程中逐渐减小学习率。
动量机制
尽管 SGD 在大规模数据上表现良好,但它仍然存在一些问题,尤其是在优化路径中存在大量震荡或噪声时。为了改善这种情况,动量机制(Momentum)被引入到梯度下降中。
动量的基本原理
动量机制的核心思想是引入一个速度变量,使得参数更新不仅依赖于当前的梯度,还依赖于之前的速度。具体来说,动量机制的更新规则为:
其中,γ是动量系数,通常取值在 0.9 左右。当γ=0时,退化为普通的SGD。以下是动量机制的 Python 实现:
def momentum(start, lr=0.1, gamma=0.9, epochs=100):
x, y = start
vx, vy = 0, 0 # 初始化速度
history = []
for _ in range(epochs):
grad_x = 2 * x # 计算梯度
grad_y = 4 * y
vx = gamma * vx + lr * grad_x # 更新速度
vy = gamma * vy + lr * grad_y
x -= vx # 更新参数
y -= vy
history.append((x, y))
return history
# 测试
start_value = (5.0, 5.0)
learning_rate = 0.1
momentum_coeff = 0.9
result = momentum(start_value, lr=learning_rate, gamma=momentum_coeff, epochs=100)
print("最终结果:", result[-1])
运行结果如下:
涅斯捷洛夫动量
涅斯捷洛夫动量(Nesterov Momentum)是动量机制的一个改进版本。它的核心思想是先根据当前的速度预估未来的参数位置,然后在该位置计算梯度。这种前瞻性的梯度计算能够进一步提高收敛速度。具体来说,其更新规则为:
以下是涅斯捷洛夫动量的 Python 实现:
import numpy as np
def nesterov_momentum(gradient, theta_init, learning_rate=0.1, momentum=0.9, num_iters=100):
"""
涅斯捷洛夫动量梯度下降算法实现
参数:
- gradient: 梯度函数,接受参数 theta,返回梯度值
- theta_init: 初始参数值
- learning_rate: 学习率 (默认 0.1)
- momentum: 动量系数 (默认 0.9)
- num_iters: 迭代次数 (默认 100)
返回:
- theta_history: 参数更新历史
- loss_history: 损失函数值历史
"""
theta = theta_init # 初始化参数
v = np.zeros_like(theta) # 初始化速度
theta_history = [theta.copy()] # 记录参数更新历史
loss_history = [] # 记录损失函数值历史
for i in range(num_iters):
# 计算前瞻位置的梯度
lookahead_theta = theta + momentum * v
grad = gradient(lookahead_theta)
# 更新速度
v = momentum * v - learning_rate * grad
# 更新参数
theta += v
# 记录历史
theta_history.append(theta.copy())
loss = theta[0]**2 + 2 * theta[1]**2 # 计算损失函数值
loss_history.append(loss)
return theta_history, loss_history
# 定义目标函数的梯度
def gradient_function(theta):
x, y = theta
return np.array([2 * x, 4 * y])
# 初始参数值
theta_init = np.array([5.0, 5.0]) # 初始点 (x, y) = (5, 5)
# 运行涅斯捷洛夫动量梯度下降
theta_history, loss_history = nesterov_momentum(gradient_function, theta_init, learning_rate=0.1, momentum=0.9, num_iters=50)
# 输出结果
print("最终参数值:", theta_history[-1])
print("最终损失值:", loss_history[-1])
# 可视化损失函数值的变化
import matplotlib.pyplot as plt
plt.plot(loss_history)
plt.xlabel("Iteration")
plt.ylabel("Loss")
plt.title("Loss over Iterations")
plt.show()
运行结果如下:
绘制出的loss曲线如下:
自适应梯度下降
随着深度学习模型的复杂性不断增加,传统的梯度下降方法在某些情况下可能表现不佳。为了应对这一问题,自适应梯度下降方法应运而生。这些方法通过动态调整每个参数的学习率,使得优化过程更加高效。
RMSprop
RMSprop 是一种常用的自适应梯度下降方法,它通过维护一个指数衰减的梯度平方均值来调整学习率。以下是 RMSprop 的 Python 实现:
def rmsprop(start, lr=0.1, decay_rate=0.9, eps=1e-8, epochs=100):
x, y = start
cache_x, cache_y = 0, 0 # 初始化缓存
history = []
for _ in range(epochs):
grad_x = 2 * x # 计算梯度
grad_y = 4 * y
cache_x = decay_rate * cache_x + (1 - decay_rate) * grad_x ** 2 # 更新缓存
cache_y = decay_rate * cache_y + (1 - decay_rate) * grad_y ** 2
x -= lr * grad_x / (np.sqrt(cache_x) + eps) # 更新参数
y -= lr * grad_y / (np.sqrt(cache_y) + eps)
history.append((x, y))
return history
# 测试
start_value = (5.0, 5.0)
learning_rate = 0.1
decay_rate = 0.9
result = rmsprop(start_value, lr=learning_rate, decay_rate=decay_rate, epochs=100)
print("最终结果:", result[-1])
运行结果如下:
Adam
Adam(Adaptive Moment Estimation)是目前最流行的自适应梯度下降方法之一。它结合了动量机制和 RMSprop 的优点,通过维护两个指数衰减的均值来调整学习率。以下是 Adam 的 Python 实现:
def adam(start, lr=0.001, beta1=0.9, beta2=0.999, eps=1e-8, epochs=100):
x, y = start
m_x, m_y = 0, 0 # 初始化一阶矩
v_x, v_y = 0, 0 # 初始化二阶矩
history = []
for t in range(1, epochs + 1):
grad_x = 2 * x # 计算梯度
grad_y = 4 * y
m_x = beta1 * m_x + (1 - beta1) * grad_x # 更新一阶矩
m_y = beta1 * m_y + (1 - beta1) * grad_y
v_x = beta2 * v_x + (1 - beta2) * grad_x ** 2 # 更新二阶矩
v_y = beta2 * v_y + (1 - beta2) * grad_y ** 2
# 偏差修正
m_x_hat = m_x / (1 - beta1 ** t)
m_y_hat = m_y / (1 - beta1 ** t)
v_x_hat = v_x / (1 - beta2 ** t)
v_y_hat = v_y / (1 - beta2 ** t)
x -= lr * m_x_hat / (np.sqrt(v_x_hat) + eps) # 更新参数
y -= lr * m_y_hat / (np.sqrt(v_y_hat) + eps)
history.append((x, y))
return history
# 测试
start_value = (5.0, 5.0)
learning_rate = 0.1
result = adam(start_value, lr=learning_rate, epochs=100)
print("最终结果:", result[-1])
运行结果如下:
可见Adam的收敛速度较慢。将epochs改为500,运行结果如下:
优化器选择指南
优化器的选择与特定的数据集有关。对于大多数的深度学习任务,Adam表现良好。但有时候,使用SGD也能调试出很好的结果。下面是关于优化器选择的总结:
td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
优化器
优点
缺点
适用场景
SGD
简单、易于调参
收敛慢、易陷入局部最优
小数据集、简单任务
SGD+Momentum
加速收敛、减少振荡
需要调参
大数据集、复杂任务
NAG
比 Momentum 更快收敛
实现稍复杂
需要快速收敛的任务
Adam
自适应学习率、默认参数表现良好
可能在某些任务上过拟合
大多数深度学习任务
RMSProp
自适应学习率、适合非平稳目标
需要调参
RNN、非平稳目标函数
Adagrad
适合稀疏数据
学习率单调下降、可能过早停止
稀疏数据集(如 NLP)
Adadelta
无需设置初始学习率
收敛速度较慢
需要自适应学习率且不想调参的场景
总结
梯度下降作为机器学习和深度学习的核心优化算法,经历了从基础到现代的不断演进。从最简单的梯度下降,到随机梯度下降、动量机制,再到自适应梯度下降方法,每一步的改进都使得优化过程更加高效和稳定。
在实际应用中,选择合适的优化算法需要根据具体问题的特点来决定。对于简单的凸优化问题,传统的梯度下降或动量机制可能已经足够;而对于复杂的深度学习模型,Adam 等自适应方法则往往能够提供更好的性能。
- 2025-02-04
-
回复了主题帖:
RDK X3测评:5.ROS2命令行操作
Jacktang 发表于 2025-2-3 10:09
ROS2命令行工具确实很强大的
确实,值得深入去学习
-
发表了主题帖:
RDK X3测评:6.ROS开发流程
ROS架构
分布式系统设计
ROS采用独特的分布式架构设计,其核心思想是将机器人系统分解为多个独立运行的节点(Node)。这种设计模式带来三大优势:
模块解耦:每个节点负责单一功能(如传感器采集、运动控制等),通过标准接口通信。
跨平台部署:节点可运行在不同硬件平台(如工控机、嵌入式设备)。
动态重构:支持运行时节点的增删改操作。
核心通信机制
通信方式
传输协议
典型应用场景
Topic(话题)
发布/订阅
持续数据流(如传感器数据)
Service(服务)
请求/响应
即时指令执行(如启停控制)
Action(动作)
状态反馈
长时任务(如导航到目标点)
环境准备与基础概念
ROS 2开发环境要求
Ubuntu 22.04
ROS 2 Humble
Python 3.8+
colcon构建工具
sudo apt install python3-colcon-common-extensions
基础概念
功能包(Package)
功能包是ROS的原子单元,用于组织代码和资源。每个功能包通常包含一个特定功能或模块的代码、配置文件、启动文件等。
一个典型的功能包目录结构如下:
package_name/
├── CMakeLists.txt
├── package.xml
├── src/
├── include/
├── launch/
├── msg/
├── srv/
├── action/
└── scripts/
各目录的功能如下:
CMakeLists.txt:用于定义包的编译规则。
package.xml:包含包的元数据,如名称、版本、依赖等。
src/:存放C++源代码。
include/:存放C++头文件。
launch/:存放启动文件,用于启动多个节点。
msg/:存放自定义消息类型。
srv/:存放自定义服务类型。
action/:存放自定义动作类型。
scripts/:存放Python脚本。
节点(Node)
节点是ROS中的可执行进程,负责执行特定的任务。节点之间通过话题(Topic)、服务(Service)或动作(Action)进行通信。
在功能包的src/目录下创建C++或Python文件,从而创建相应的节点,例如my_node.cpp或my_node.py。
工作空间(Workspace)
工作空间是ROS开发的容器,用于组织和管理多个功能包。典型的工作空间结构如下:
workspace/
├── src/
├── install/
├── log/
└── build/
其中各目录的功能如下:
src/:存放所有功能包的源代码。
install/:存放编译后的可执行文件和库。
log/:存放日志文件。
build/:存放编译过程中生成的中间文件。
Python开发流程
以下是基于python的详细开发流程,包括功能包创建、包结构说明、节点开发以及编译配置等。
创建Python功能包
在ROS 2中,使用以下命令创建一个Python功能包:
source /opt/tros/humble/setup.bash
ros2 pkg create --build-type ament_python demo_py --dependencies rclpy std_msgs
--build-type ament_python:指定这是一个Python功能包,使用ament构建系统。
demo_py:功能包的名称。
--dependencies rclpy std_msgs:指定功能包的依赖项,rclpy是ROS 2的Python客户端库,std_msgs是标准消息类型。
创建的包结构
创建的功能包结构如下:
文件说明:
demo_py/:功能包的根目录。
demo_py/:Python模块目录,与功能包同名。
__init__.py:使该目录成为一个Python模块。
node_demo.py:Python节点代码文件(自行创建)。
package.xml:功能包的元数据文件,包含名称、版本、依赖等信息。
setup.cfg:配置Python包的安装方式。
setup.py:定义Python包的构建和安装规则。
Python节点开发示例
以下是一个简单的Python节点示例,发布消息到ROS 2话题。
# node_demo.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__('py_talker')
self.publisher = self.create_publisher(String, 'topic', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS2: {self.count}'
self.publisher.publish(msg)
self.get_logger().info(f'Published: {msg.data}')
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = Talker()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
代码说明:
Talker类:继承自Node,表示一个ROS 2节点。
create_publisher:创建一个发布者,发布String类型的消息到topic话题。
create_timer:创建一个定时器,每1秒触发一次timer_callback。
timer_callback:定时器回调函数,发布消息并打印日志。
main函数:初始化ROS 2,创建节点并运行。
配置编译规则
在ROS 2中,Python节点的编译和安装规则通过setup.py文件配置。
# setup.py关键配置
entry_points={
'console_scripts': [
'py_node = demo_py.node_demo:main',
],
}
entry_points:定义可执行脚本。
py_node:可执行命令的名称。
demo_py.node_demo:main:指定执行的Python模块和函数。
编译和运行节点
编译功能包
在工作空间根目录下运行以下命令:
colcon build --packages-select demo_py
colcon是ROS 2的构建工具。
--packages-select demo_py:仅编译demo_py功能包。
编译完成后的目录如下:
节点测试
首先我们运行创建的py_node节点。
#加载工作空间环境
source install/setup.bash
#运行节点
ros2 run demo_py py_node
输出如下:
可以看到每隔一秒会发布一个“Hello ROS2”的信息。
之后我们创建一个新的终端,查看我们创建的节点是否能够正常工作。首先查看话题列表
#查看话题列表
ros2 topic list
可以看到当前存在topic话题,之后查看话题内容
#查看话题内容
ros2 topic echo /topic
可以看到,当前话题中能够接收到我们创建的节点发送的信息,说明我们的节点运行功能正常。
C++开发流程
在ROS 2中,使用C++开发功能包和节点的流程与Python类似,但涉及更多的编译配置。以下是详细的C++开发全流程,包括功能包创建、节点开发、包结构说明以及编译配置。
创建C++功能包
使用以下命令创建一个C++功能包:
ros2 pkg create --build-type ament_cmake demo_cpp \--dependencies rclcpp std_msgs
--build-type ament_cmake:指定这是一个C++功能包,使用ament_cmake构建系统。
demo_cpp:功能包的名称。
--dependencies rclcpp std_msgs:指定功能包的依赖项,rclcpp是ROS 2的C++客户端库,std_msgs是标准消息类型。
创建的包结构
创建的功能包结构如下:
文件说明:
CMakeLists.txt:定义C++项目的编译规则。
include/:存放C++头文件。
src/:存放C++源代码文件。
package.xml:功能包的元数据文件,包含名称、版本、依赖等信息。
C++节点开发示例
以下是一个简单的C++节点示例,在src目录下创建cpp_node.cpp文件,订阅ROS 2话题并打印接收到的消息。
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class Listener : public rclcpp::Node {
public:
Listener() : Node("cpp_listener") {
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10,
[this](const std_msgs::msg::String::SharedPtr msg) {
RCLCPP_INFO(this->get_logger(), "Received: '%s'",
msg->data.c_str());
});
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<Listener>());
rclcpp::shutdown();
return 0;
}
代码说明:
Listener类:继承自Node,表示一个ROS 2节点。
create_subscription:创建一个订阅者,订阅String类型的消息。
回调函数:接收到消息后打印日志。
main函数:初始化ROS 2,创建节点并运行。
配置编译规则
在ROS 2中,C++节点的编译规则通过CMakeLists.txt文件配置。CMakeLists.txt 关键配置如下
cmake_minimum_required(VERSION 3.8)
project(demo_cpp)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(cpp_node src/cpp_node.cpp)
ament_target_dependencies(cpp_node rclcpp std_msgs)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
代码说明:
find_package:查找所需的依赖包。
add_executable:定义可执行文件。
ament_target_dependencies:为目标添加依赖。
ament_package:导出功能包的依赖。
编译和运行节点
使用colcon构建工具编译功能包
colcon build --symlink-install
其中,--symlink-install参数为:使用符号链接而不是复制文件。
之后运行节点
source install/setup.bash
ros2 run demo_cpp cpp_node
运行结果如下:
可以看到,此时能够正常接收到上面python节点发送的信息,说明我们的cpp_node节点能够正常工作。
- 2025-02-01
-
发表了主题帖:
RDK X3测评:5.ROS2命令行操作
ROS2(Robot Operating System 2)作为机器人开发领域的核心工具,其命令行操作是开发者与系统交互的重要桥梁。无论是启动节点、管理话题,还是调试参数、记录数据,ROS2命令行工具都提供了强大的支持。而TogetherROS是基于ROS2深度优化的,很多模块还是会复用ROS2中的功能,所有ROS2的原生功能也可以支持。本文将从整体上学习了解ROS2命令行的功能,并通过详细的示例和解释,深入分析其核心操作,全面掌握ROS2命令行的使用技巧。
ROS2命令行概览
ROS2的命令行工具基于ros2命令,它是ROS2生态系统的核心接口。通过ros2命令,开发者可以完成以下主要任务:
节点管理:启动、查看和调试节点。
话题操作:查看话题列表、监听话题消息、发布消息等。
服务操作:调用服务、查看服务列表。
参数管理:获取、设置和修改节点参数。
数据记录与回放:使用ros2 bag工具记录和回放话题数据。
系统调试:查看节点关系图、监控系统状态。
在命令行中直接输入ros2,可以看到相关命令的使用介绍。
接下来,我们将以上一节中的publisher_member_function和subscriber_member_function为例,逐步深入分析这些功能。
启动节点
启动Publisher节点
首先,我们启动一个发布者节点:
source /opt/tros/humble/setup.bash
ros2 run examples_rclcpp_minimal_publisher publisher_member_function
examples_rclcpp_minimal_publisher是包名,publisher_member_function是节点名(这里不起作用,节点名实际为minimal_publisher)。
这个节点会周期性地向/topic话题发布消息。
启动Subscriber节点
接下来,我们启动一个订阅者节点:
source /opt/tros/humble/setup.bash
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function
examples_rclcpp_minimal_subscriber是包名,subscriber_member_function是节点名(这里不起作用,节点名实际为minimal_subscriber)。
这个节点会订阅/topic话题,并打印接收到的消息。
节点管理
查看节点列表
启动节点后,可以使用如下命令查看当前运行的节点:
ros2 node list
输出示例:
这表示当前系统中运行了两个节点:/minimal_publisher和/minimal_subscriber。
查看节点信息
通过如下命令,可以获取节点的详细信息。例如,查看发布者节点的信息:
ros2 node info /minimal_publisher
输出示例:
这里显示了/minimal_publisher节点的发布者和服务信息。
同样,查看订阅者节点的信息:
ros2 node info /minimal_subscriber
输出示例:
这里显示了/minimal_subscriber节点的订阅者和服务信息。
话题操作
查看话题列表
使用如下命令可以查看当前所有的话题:
ros2 topic list
输出示例:
这里列出了系统中所有活跃的话题,包括/topic。
监听话题消息
通过如下命令,可以实时查看某个话题的消息内容。例如,监听/topic话题:
ros2 topic echo /topic
输出示例:
这里显示了/topic话题上发布的消息内容。
发布消息
使用ros2 topic pub命令,可以手动向某个话题发布消息。例如,向/topic话题发布一条消息:
ros2 topic pub /topic std_msgs/msg/String "{data: 'Hello from CLI'}"
这行命令向/topic话题发布了一条消息,内容为Hello from CLI。在subscriber端我们可以看到,接收到了同样的消息。
参数管理:配置节点的行为
查看参数列表
使用如下命令可以查看某个节点的所有参数。例如,查看发布者节点的参数:
ros2 param list /minimal_publisher
输出示例:
获取参数值
通过命令如下,可以获取某个参数的值。例如,获取use_sim_time参数的值:
ros2 param get /minimal_publisher use_sim_time
输出示例:
设置参数值
使用如下命令,可以修改某个参数的值。例如,将use_sim_time参数设置为True:
ros2 param set /minimal_publisher use_sim_time True
这行命令将/minimal_publisher节点的use_sim_time参数设置为True。
数据记录与回放
记录话题数据
使用ros2 bag record命令可以记录指定话题的数据。例如,记录/topic话题的数据:
ros2 bag record /topic
这行命令会开始记录/topic话题上的所有消息,并将其保存到一个bag文件中。
查看bag信息
通过如下命令,可以查看之前记录的bag文件信息:
ros2 bag info rosbag2_2025_02_01-19_50_01
输出示例:
回放话题数据
通过如下命令,可以回放之前记录的bag文件:
ros2 bag play rosbag2_2025_02_01-19_16_17
这行命令会回放指定bag文件中的数据,模拟话题消息的发布。
打开另一个终端,使用ros2 topic echo监听/topic话题:
ros2 topic echo /topic
我们可以看到以下的输出
系统调试
查看节点关系图
使用rqt_graph工具可以可视化节点、话题和服务之间的关系:
#安装
sudo apt-get install ros-humble-rqt
sudo apt-get install ros-humble-rqt-common-plugins
#运行
rqt_graph
这个命令会打开一个图形化界面,显示当前系统中所有节点和话题的连接关系。
监控系统状态
通过ros2 doctor命令,可以检查系统的健康状况:
ros2 doctor
这个命令会生成一份报告,列出系统中可能存在的问题。
总结
通过本文的详细介绍,我们以publisher_member_function和subscriber_member_function为例,初步探索了ROS2命令行的核心功能。从节点管理到话题操作,从参数配置到数据记录,ROS2命令行工具为开发者提供了强大的支持。
-
回复了主题帖:
《深度学习的数学——使用Python语言》10.全连接网络的反向传播
秦天qintian0303 发表于 2025-2-1 08:20
反向传播是一步一步反算,还是直接和反馈似的
反向传播是从输出层开始,一步一步往回计算误差,并调整每一层的参数。它利用了链式法则,每一步都依赖前一步的结果,是逐步反算的。
- 2025-01-31
-
发表了主题帖:
《深度学习的数学——使用Python语言》10.全连接网络的反向传播
基于书中第十章,本节中,我们将深入学习反向传播的原理,并通过MNIST手写数字识别任务,结合PyTorch代码实现,手动编写反向传播逻辑,从而加深对于反向传播内部机制的理解。
神经网络与反向传播的基本概念
神经网络是一种由多层神经元组成的计算模型,每一层神经元通过权重和偏置连接起来。神经网络的学习过程可以分为两个阶段:前向传播和反向传播。
前向传播:输入数据从输入层经过隐藏层,最终到达输出层,计算网络的预测结果。
反向传播:根据预测结果与真实标签之间的误差,从输出层逐层向前计算梯度,并更新网络的权重和偏置。
反向传播的核心是链式法则,它通过将误差从输出层传递回输入层,计算每一层参数的梯度,从而指导参数的更新。
MNIST手写数字识别任务
MNIST是一个经典的手写数字识别数据集,包含60,000张训练图像和10,000张测试图像,每张图像是一个28x28的灰度图,标签为0到9的数字。我们的目标是构建一个神经网络,能够正确识别这些手写数字。
反向传播的数学原理
为了更好地理解反向传播,我们需要从数学角度分析它的工作原理。假设我们有一个简单的两层神经网络,输入层大小为784(28x28),隐藏层大小为128,输出层大小为10(对应10个数字类别)。
前向传播
前向传播的计算过程如下:
输入层到隐藏层:
其中,X是输入数据,W1是输入层到隐藏层的权重矩阵,b1是偏置,σ是激活函数(如ReLU)。
隐藏层到输出层:
其中,W2是隐藏层到输出层的权重矩阵,b2是偏置,softmax函数用于将输出转换为概率分布。
损失函数
我们使用交叉熵损失函数来衡量预测结果与真实标签之间的差异:
其中,yi是真实标签的one-hot编码,a2i是输出层的预测概率。
反向传播
反向传播的目标是计算损失函数对每一层参数的梯度,并更新参数。具体步骤如下:
计算输出层的误差:
计算隐藏层的误差:
其中,σ′是激活函数的导数。
计算梯度并更新参数:
使用梯度下降法更新参数:
其中,η是学习率。
代码实现
下面我们通过PyTorch实现一个简单的两层神经网络,并手动编写反向传播逻辑,同时利用CUDA加速训练过程。
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# 检查CUDA是否可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
# 定义数据预处理
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
# 加载MNIST数据集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 定义神经网络模型
class SimpleNN:
def __init__(self, input_size, hidden_size, output_size):
# 初始化参数
self.W1 = torch.randn(input_size, hidden_size, device=device) * 0.01
self.b1 = torch.zeros(1, hidden_size, device=device)
self.W2 = torch.randn(hidden_size, output_size, device=device) * 0.01
self.b2 = torch.zeros(1, output_size, device=device)
def forward(self, X):
# 前向传播
self.z1 = torch.matmul(X, self.W1) + self.b1
self.a1 = torch.relu(self.z1)
self.z2 = torch.matmul(self.a1, self.W2) + self.b2
self.a2 = torch.softmax(self.z2, dim=1)
return self.a2
def backward(self, X, y, output, learning_rate):
# 反向传播
m = X.shape[0]
# 输出层误差
dz2 = output - y
dW2 = torch.matmul(self.a1.T, dz2) / m
db2 = torch.sum(dz2, dim=0, keepdim=True) / m
# 隐藏层误差
dz1 = torch.matmul(dz2, self.W2.T) * (self.a1 > 0).float()
dW1 = torch.matmul(X.T, dz1) / m
db1 = torch.sum(dz1, dim=0, keepdim=True) / m
# 更新参数
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
def train(self, train_loader, epochs, learning_rate):
for epoch in range(epochs):
for images, labels in train_loader:
# 将图像展平并移动到GPU
images = images.view(-1, 28*28).to(device)
# 将标签转换为one-hot编码并移动到GPU
y = torch.zeros(labels.size(0), 10, device=device)
y[torch.arange(labels.size(0)), labels] = 1
# 前向传播
output = self.forward(images)
# 反向传播
self.backward(images, y, output, learning_rate)
if (epoch+1) % 5 == 0:
print(f'Epoch [{epoch+1}/{epochs}]')
def evaluate(self, test_loader):
correct = 0
total = 0
for images, labels in test_loader:
images = images.view(-1, 28*28).to(device)
outputs = self.forward(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted.cpu() == labels).sum().item()
print(f'Test Accuracy: {100 * correct / total:.2f}%')
# 初始化模型
input_size = 28 * 28
hidden_size = 128
output_size = 10
model = SimpleNN(input_size, hidden_size, output_size)
# 训练模型
model.train(train_loader, epochs=20, learning_rate=0.1)
# 测试模型
model.evaluate(test_loader)
在SimpleNN的init部分,W1 和 W2 分别是输入层到隐藏层和隐藏层到输出层的权重矩阵。我们使用 torch.randn 生成服从标准正态分布的随机数,并乘以 0.01 来缩小初始值的范围。b1 和 b2 是偏置,初始化为零。所有参数都被放置在指定的设备(如GPU)上。
在forward()中,实现了前向传播的过程。首先,输入数据 X 与权重矩阵 W1 进行矩阵乘法,再加上偏置 b1,得到隐藏层的加权输入 z1。然后,通过ReLU激活函数对 z1 进行非线性变换,得到隐藏层的激活值 a1。之后,隐藏层的激活值 a1 与权重矩阵 W2 进行矩阵乘法,再加上偏置 b2,得到输出层的加权输入 z2。最后,通过softmax函数将 z2 转换为概率分布 a2,表示每个类别的预测概率。
在反向传播中,通过链式法则,误差从输出层逐层向前传递,计算每一层的梯度。首先,计算输出层的误差 dz2,即预测值 output 与真实标签 y 的差值。然后,计算权重 W2 的梯度 dW2,通过将隐藏层的激活值 a1 的转置与误差 dz2 相乘,并除以样本数 m。偏置 b2 的梯度 db2 是误差 dz2 的均值。之后,计算隐藏层的误差 dz1,通过将输出层的误差 dz2 与权重矩阵 W2 的转置相乘,再乘以ReLU激活函数的导数(即 a1 > 0 的布尔值转换为浮点数)。然后,计算权重 W1 的梯度 dW1,通过将输入数据 X 的转置与误差 dz1 相乘,并除以样本数 m。偏置 b1 的梯度 db1 是误差 dz1 的均值。最后,使用梯度下降法更新参数。权重和偏置分别减去学习率与对应梯度的乘积。
运行结果如下,可以看到通过上述代码,我们能够得到较高的准确率。
深入思考
反向传播是神经网络训练的核心算法,但它并非完美无缺。在实际应用中,我们可能会遇到以下问题:
梯度消失:在深层网络中,梯度可能会逐渐变小,导致靠近输入层的参数几乎无法更新。使用ReLU激活函数和批量归一化可以有效缓解这一问题。
过拟合:神经网络容易过拟合训练数据。可以通过正则化(如L2正则化)和Dropout来减少过拟合。
计算效率:反向传播的计算复杂度较高,尤其是在大规模数据集和深层网络中。使用GPU加速和分布式计算可以显著提高训练速度。
总结
通过MNIST手写数字识别任务,我们从理论和代码两个层面深入探讨了反向传播的原理和实现。不仅学习了反向传播的数学原理,还通过手动编写反向传播逻辑,更好地理解了其内部机制。
- 2025-01-30
-
发表了主题帖:
《深度学习的数学——使用Python语言》9. 矩阵微分之“雅可比矩阵与黑塞矩阵”
在深度学习中,导数在优化算法、梯度计算、反向传播等方面起着至关重要的作用。雅可比矩阵(Jacobian Matrix)和黑塞矩阵(Hessian Matrix)是多元微积分中的两个重要概念,理解它们的计算方法及应用对掌握深度学习至关重要。
雅可比矩阵
定义
在向量微积分中,雅可比矩阵是一阶偏导数以一定方式排列成的矩阵,其行列式称为雅可比行列式。
雅可比矩阵的重要性在于它能够捕捉一个可微方程在某一点的最优线性逼近。要理解这一点,我们可以从一个向量值函数的角度出发。假设我们有一个函数F:Rn→Rm,它将一个n维向量映射到一个m维向量。如果这个函数在某一点a是可微的,那么在这一点附近,函数的行为可以用一个线性映射来近似。而这个线性映射的核心就是雅可比矩阵。
雅可比矩阵是由函数F的所有一阶偏导数组成的矩阵,记作J(a)。它的每一行对应函数的一个分量,每一列对应一个输入变量。通过这个矩阵,我们可以将函数在a点附近的变化用线性形式表达出来。具体来说,当我们在a点附近取一个微小的增量h,函数的值可以近似表示为F(a+h)≈F(a)+J(a)h。这个近似是“最优”的,因为雅可比矩阵提供的线性映射能够最精确地描述函数在这一点附近的局部行为。
雅可比矩阵的意义
从几何上看,雅可比矩阵的作用类似于单变量函数中的导数。它告诉我们,当输入发生微小变化时,输出会如何变化。这种线性逼近的能力使得我们能够用相对简单的线性工具来处理复杂的非线性问题。无论是优化问题、非线性方程组的求解,还是机器学习中的梯度计算,雅可比矩阵都扮演着关键角色。它帮助我们理解系统的局部动态,并为复杂问题的求解提供了有效的数学工具。
雅可比矩阵的应用
在深度学习中,雅可比矩阵的一个重要应用是梯度计算和反向传播算法。深度学习模型的训练依赖于损失函数对模型参数的梯度,而雅可比矩阵在这个过程中起到了关键作用。具体来说,神经网络的每一层都可以看作一个非线性函数,而整个网络是由这些函数复合而成的复杂映射。为了计算损失函数对参数的梯度,我们需要通过链式法则将每一层的雅可比矩阵相乘。
使用 autograd 库计算雅可比矩阵
下面是一个简单的使用 autograd 库计算雅可比矩阵的例子:
import torch
import torch.nn as nn
import torch.optim as optim
# 定义一个简单的线性层
class SimpleLinearLayer(nn.Module):
def __init__(self, input_dim, output_dim):
super(SimpleLinearLayer, self).__init__()
self.weight = nn.Parameter(torch.randn(output_dim, input_dim))
self.bias = nn.Parameter(torch.randn(output_dim))
def forward(self, x):
return torch.matmul(self.weight, x) + self.bias
# 定义一个简单的损失函数(均方误差)
def loss_function(y_pred, y_true):
return torch.mean((y_pred - y_true) ** 2)
# 输入数据
x = torch.tensor([1.0, 2.0], requires_grad=False)
y_true = torch.tensor([3.0], requires_grad=False)
# 初始化模型
model = SimpleLinearLayer(input_dim=2, output_dim=1)
# 前向传播
y_pred = model(x)
# 计算损失
loss = loss_function(y_pred, y_true)
# 手动计算雅可比矩阵
# 损失函数对 y_pred 的梯度
dL_dy_pred = 2 * (y_pred - y_true) / y_pred.size(0)
# y_pred 对权重的雅可比矩阵
# y_pred = W * x + b,因此 dy_pred/dW = x^T
dy_pred_dW = x.unsqueeze(0) # 将 x 转换为行向量
# 使用链式法则计算损失对权重的梯度
dL_dW = torch.matmul(dL_dy_pred.unsqueeze(1), dy_pred_dW)
print("损失值:", loss.item())
print("损失对权重的梯度:", dL_dW)
运行结果如下:
根据雅可比矩阵的数学定义计算,得出的结果与上述结果相符。
使用 PyTorch 计算雅可比矩阵
以下是一个简单的例子,展示如何使用雅可比矩阵计算梯度。我们使用 PyTorch 来实现一个简单的神经网络,并手动计算雅可比矩阵。
import torch
import torch.nn as nn
import torch.optim as optim
# 定义一个简单的线性层
class SimpleLinearLayer(nn.Module):
def __init__(self, input_dim, output_dim):
super(SimpleLinearLayer, self).__init__()
self.weight = nn.Parameter(torch.randn(output_dim, input_dim))
self.bias = nn.Parameter(torch.randn(output_dim))
def forward(self, x):
return torch.matmul(self.weight, x) + self.bias
# 定义一个简单的损失函数(均方误差)
def loss_function(y_pred, y_true):
return torch.mean((y_pred - y_true) ** 2)
# 输入数据
x = torch.tensor([1.0, 2.0], requires_grad=False)
y_true = torch.tensor([3.0], requires_grad=False)
# 初始化模型
model = SimpleLinearLayer(input_dim=2, output_dim=1)
# 前向传播
y_pred = model(x)
# 计算损失
loss = loss_function(y_pred, y_true)
# 手动计算雅可比矩阵
# 损失函数对 y_pred 的梯度
dL_dy_pred = 2 * (y_pred - y_true) / y_pred.size(0)
# y_pred 对权重的雅可比矩阵
# y_pred = W * x + b,因此 dy_pred/dW = x^T
dy_pred_dW = x.unsqueeze(0) # 将 x 转换为行向量
# 使用链式法则计算损失对权重的梯度
dL_dW = torch.matmul(dL_dy_pred.unsqueeze(1), dy_pred_dW)
print("损失值:", loss.item())
print("损失对权重的梯度:", dL_dW)
运行结果如下:
在反向传播部分,为了计算损失对权重W的梯度,我们需要用到链式法则。链式法则告诉我们,损失对权重的梯度可以分解为两部分:
损失对输出 y_pred 的梯度:对于均方误差损失函数,其梯度为 2(y_pred−y_true)。
输出 y_pred 对权重W的雅可比矩阵:在这个简单的线性层中,y_pred=Wx+b,因此 y_pred 对W的雅可比矩阵就是输入x的转置。
通过将这两部分相乘,我们得到了损失对权重W的梯度。这个过程展示了雅可比矩阵在反向传播中的作用:它将复杂的非线性映射分解为一系列线性变换,从而使得梯度的计算变得可行。
黑塞矩阵
在深度学习中,优化是模型训练的核心问题之一。我们通常使用梯度下降法来最小化损失函数,但梯度下降法只依赖于一阶导数(梯度),而忽略了二阶导数信息。为了更深入地理解损失函数的几何性质,并设计更高效的优化算法,我们需要引入黑塞矩阵(Hessian Matrix)。黑塞矩阵是损失函数的二阶导数矩阵,它包含了函数的曲率信息,能够帮助我们更好地理解优化问题的复杂性。
定义
黑塞矩阵是一个由函数二阶偏导数组成的方阵。对于一个标量函数f(x),其中 x=[x1,x2,…,xn]^T是一个n维向量,黑塞矩阵H定义为
黑塞矩阵的每个元素 Hij表示函数f在xi和xj方向上的二阶偏导数。它描述了函数在某一点的局部曲率。
黑塞矩阵的意义
曲率信息:黑塞矩阵的特征值可以告诉我们函数在某一点的曲率。如果特征值都为正,说明函数在该点是局部凸的;如果有正有负,说明函数在该点是鞍点;如果特征值都为零,说明函数在该点是平坦的。
优化方向:在深度学习中,黑塞矩阵可以帮助我们设计更高效的优化算法。例如,牛顿法就是利用黑塞矩阵的逆来更新参数,从而更快地收敛到局部最小值。
鞍点问题:在高维空间中,鞍点比局部最小值更常见。黑塞矩阵的特征值可以帮助我们判断当前点是局部最小值还是鞍点,从而指导优化过程。
黑塞矩阵的应用
在深度学习中,最常用到黑塞矩阵的地方就是求解优化问题。以下是一个简单的 Python 示例,展示如何计算黑塞矩阵并使用牛顿法优化一个二次函数。
import numpy as np
from scipy.optimize import minimize
# 定义一个二次函数 f(x) = x^T A x + b^T x + c
A = np.array([[3, 2], [2, 6]]) # 正定矩阵
b = np.array([2, -8])
c = 10
def f(x):
return 0.5 * x.T @ A @ x + b.T @ x + c
def gradient(x):
return A @ x + b
def hessian(x):
return A # 对于二次函数,黑塞矩阵是常数矩阵
# 初始点
x0 = np.array([0, 0])
# 使用牛顿法优化
result = minimize(f, x0, method='Newton-CG', jac=gradient, hess=hessian)
print("最优解:", result.x)
print("最优值:", result.fun)
运行结果如下:
代码解释如下:
函数定义:我们定义了一个二次函数 f(x),其中A是一个正定矩阵,确保函数有唯一的全局最小值。
梯度和黑塞矩阵:对于二次函数,梯度是 ∇f(x)=Ax+b,黑塞矩阵是常数矩阵A。
牛顿法优化:我们使用 SciPy 库中的 minimize 函数,并指定 method='Newton-CG' 来使用牛顿法进行优化。牛顿法利用黑塞矩阵的信息来加速收敛。
黑塞矩阵的问题
然而,在深度学习中之所以使用梯度下降法广泛替代直接使用黑塞矩阵的方法,主要原因可以从黑塞矩阵的正定性问题和计算复杂度两个方面来解释。
正定性
黑塞矩阵的正定性直接关系到优化算法的稳定性和收敛性。
正定黑塞矩阵:如果黑塞矩阵是正定的,说明函数在该点是局部凸的,存在唯一的局部最小值。此时,牛顿法等基于黑塞矩阵的优化算法可以快速收敛。
非正定黑塞矩阵:如果黑塞矩阵不是正定的,函数在该点可能是鞍点或局部最大值。在这种情况下,牛顿法可能会失效,因为黑塞矩阵的逆可能不存在或不稳定,导致参数更新方向错误。
在深度学习中,损失函数的几何形状通常非常复杂,黑塞矩阵的正定性无法保证。特别是在高维空间中,鞍点比局部最小值更常见。这使得直接使用黑塞矩阵的优化方法(如牛顿法)在实际应用中变得不可靠。
计算复杂度
黑塞矩阵的计算和存储是另一个巨大的挑战。对于一个有n个参数的模型,黑塞矩阵是一个n×n的矩阵。对于现代深度学习模型,参数数量n可能达到数百万甚至数十亿,这使得黑塞矩阵的计算和存储变得极其昂贵。
计算黑塞矩阵:计算黑塞矩阵需要计算所有二阶偏导数,其时间复杂度为O(n^2)。对于大规模模型,这几乎是不可行的。
存储黑塞矩阵:存储一个n×n的矩阵需要 O(n^2)的内存空间。对于 n=10^6,黑塞矩阵需要 10^12个元素的存储空间,这远远超出了现代硬件的容量。
黑塞矩阵的逆:即使能够计算黑塞矩阵,求其逆矩阵的时间复杂度为 O(n^3),这在大规模问题中是完全不可行的。
相比之下,梯度下降法只需要计算一阶导数(梯度),其时间复杂度为 O(n),并且只需要存储梯度向量,空间复杂度也是 O(n)。这使得梯度下降法能够轻松扩展到大规模问题。
总结
在深度学习中,矩阵求导是理解和实现优化算法的关键。雅可比矩阵和黑塞矩阵分别提供了一阶和二阶导数的信息,帮助我们理解函数的局部行为和曲率。
雅可比矩阵:用于描述向量值函数的一阶导数,广泛应用于梯度计算和反向传播。通过链式法则,雅可比矩阵使得复杂函数的梯度计算变得可行。
黑塞矩阵:提供了函数的二阶导数信息,帮助我们理解函数的曲率和优化方向。尽管黑塞矩阵在理论上提供了更丰富的优化信息,但其计算和存储复杂度限制了其在大规模深度学习中的应用。
在实际应用中,梯度下降法及其变种(如随机梯度下降、Adam 等)因其计算效率和可扩展性,成为了深度学习中的主流优化算法。然而,理解雅可比矩阵和黑塞矩阵的理论基础,对于深入掌握深度学习中的优化过程仍然至关重要。
- 2025-01-29
-
回复了主题帖:
EEWORLD陪你过大年,新年积分兑换盲盒收到啦-UFUN
我也抽中了这个板子,学习了!
-
发表了主题帖:
RDK X3测评:4.TogetheROS.Bot安装
TogetheROS.Bot是D-Robotics面向机器人厂商和生态开发者推出的机器人操作系统,旨在释放机器人场景的智能潜能,助力生态开发者和商业客户能够高效、便捷的进行机器人开发,打造具有竞争力的智能机器人产品。TogetheROS.Bot主要特性如下:
提供“hobot_sensor”适配机器人常用传感器,节省开发时间,聚焦核心竞争力。
提供“hobot_dnn”简化板端算法模型推理与部署,释放BPU算力,降低智能算法使用门槛。
提供“hobot_codec”软硬结合加速视频编解码,节省CPU资源,提升并行处理能力。
提供“hobot_cv”软硬结合提升常见CV算子性能,节省CPU资源,提升运行效率。
提供“hobot Render”Web端和HDMI动态可视化功能,实时渲染算法结果(仅限Web端),便于展示与调试。
增加“zero-copy”进程间零拷贝通信机制,降低数据传输时延,减少系统资源消耗。
丰富中间件软件调试以及性能调优工具,提升问题定位效率,方便系统性能优化。
与ROS2 Foxy/Humble版本接口保持完全兼容,便于复用ROS丰富工具包,加快原型验证。
支持最小化和模块化剪裁,方便根据需要部署在资源受限的嵌入式产品中。
安装
文档中提供了tros-foxy和tros-humble两种功能包的安装教程,其中tros-humble相对更新,包含更多新的功能。从更新日志我们也可以看到,官方对于tros-humble的更新最新到25年1月,而对于tros-foxy的更新截止到24年3月。因此我们这里以tros-humble为例进行安装。
首先需要确保开发板已经接入互联网,之后使用apt命令进行安装
sudo apt update
sudo apt install tros-humble
此外,tros.b还有1.x和2.x版本之分,官方给出的说明是2.x版本tros.b仅支持2.x版本系统;支持RDK、RDK Module等全系列硬件;未来tros.b的新增功能将会发布在2.x版本tros.b;代码托管在github。而1.x版本tros.b为历史版本,仅支持1.x版本系统和RDK,未来1.x版本tros.b仅发布问题修复版本。两者的功能和使用差别如下:
我们可以使用apt show命令来查看我们安装的tros版本。
可以看到,我们目前安装的是2..3.1版本的tros。
测试
下面运行一个示例程序来确认tros.b是否安装成功。首先安装Hello World示例对应的package
sudo apt update
sudo apt install ros-humble-examples-rclcpp-minimal-publisher ros-humble-examples-rclcpp-minimal-subscriber
之后打开两个终端,分别运行
source /opt/tros/humble/setup.bash
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function
source /opt/tros/humble/setup.bash
ros2 run examples_rclcpp_minimal_publisher publisher_member_function
运行结果如下:
可以看到一个终端在不断发送“'Hello, world!”,另一个终端作为sub端不断收到“'Hello, world!”,说明两个终端之间能够正常通信,我们的tros.b安装正确。
接下来我们将继续深入学习ros的使用。
- 2025-01-28
-
发表了主题帖:
RDK X3测评:3.模型推理
本帖最后由 waterman 于 2025-1-28 23:53 编辑
本节将基于RDK Python 语言的hobot_dnn模型推理库,完成静态图片推理学习,包括模型加载、数据预处理、模型推理、算法结果后处理等操作。hobot_dnn推理库主要使用的类和接口如下:
Model : 算法模型类,执行加载算法模型、推理计算。
pyDNNTensor:算法数据输入、输出数据 tensor 类。
TensorProperties :模型输入 tensor 的属性类。
load:加载算法模型。
模型推理流程
开发板Ubuntu系统预装了Python版本的pyeasy_dnn模型推理模块,通过加载模型并创建Model对象,完成模型推理、数据解析等功能。
模型加载
from hobot_dnn import pyeasy_dnn as dnn
#create model object
models = dnn.load('./model.bin')
图像推理
#do inference with image
outputs = models[0].forward(image)
数据解析
for item in outputs:
output_array.append(item.buffer)
post_process(output_array)
其中在图像推理部分,我们需要输入待处理的图像,但这个图像的大小我们应该如何查看呢,可以使用Model.inputs进行查看。以yolov5s_672x672_nv12.bin为例,在app/pydev_demo目录下运行如下代码,查看模型的tensor输入信息。
from hobot_dnn import pyeasy_dnn as dnn
def print_properties(pro):
print("tensor type:", pro.tensor_type)
print("data type:", pro.dtype)
print("layout:", pro.layout)
print("shape:", pro.shape)
models = dnn.load('./models/yolov5s_672x672_nv12.bin')
input = models[0].inputs[0]
print_properties(input.properties)
输出结果如下:
其中NV12指的是图像格式,属于 YUV 颜色空间中的 YUV420SP 格式,每四个 Y 分量共用一组 U 分量和 V 分量,Y 连续存放,U 与 V 交叉存放。数据类型为uint8,数据格式为NCHW,既Batch*Channel*Height*Width,输入的形状为1*3*672*672。
此外,我们还能使用Model.outputs来查看模型的输出数据属性,具体代码如下:
from hobot_dnn import pyeasy_dnn as dnn
def print_properties(pro):
print("tensor type:", pro.tensor_type)
print("data type:", pro.dtype)
print("layout:", pro.layout)
print("shape:", pro.shape)
models = dnn.load('./models/yolov5s_672x672_nv12.bin')
output = models[0].outputs[0]
print_properties(output.properties)
输出结果如下图所示:
其中与输入有所不同,数据类型为float32,数据格式为NHWC,既Batch*Height*Width*Channel,输出的形状为1*84*84*255。
下面我们就以yolov5s_672x672_nv12模型为例,进行静态图片推理的实现。
静态图片推理
首先导入相关的库
import numpy as np
import cv2
from hobot_dnn import pyeasy_dnn as dnn
import time
import ctypes
import json
其中,time用于测量代码执行时间,ctypes提供了Python与C语言动态链接库交互的桥梁,json用于解析JSON格式的数据。
加载并配置后处理库
libpostprocess = ctypes.CDLL('/usr/lib/libpostprocess.so')
get_Postprocess_result = libpostprocess.Yolov5PostProcess
get_Postprocess_result.argtypes = [ctypes.POINTER(Yolov5PostProcessInfo_t)]
get_Postprocess_result.restype = ctypes.c_char_p
其中libpostprocess.so是官方提供的一个后处理动态库,其中包含了对Yolov5模型输出结果的后处理,需要使用ctypes调用。
读取模型并打印输入输出张量属性
models = dnn.load('../models/yolov5s_672x672_nv12.bin')
# 打印输入 tensor 的属性
print_properties(models[0].inputs[0].properties)
# 打印输出 tensor 的属性
print(len(models[0].outputs))
for output in models[0].outputs:
print_properties(output.properties)
图像读取与预处理
img_file = cv2.imread('./kite.jpg')
h, w = get_hw(models[0].inputs[0].properties)
des_dim = (w, h)
resized_data = cv2.resize(img_file, des_dim, interpolation=cv2.INTER_AREA)
nv12_data = bgr2nv12_opencv(resized_data)
这里使用kite.jpg图片进行推理,但是由于图片与模型的输出可能不同,因此需要调用cv2.resize对图像进行放缩,并且需要将图片格式转换为我们在上面看到的nv12的格式。
模型推理
t0 = time.time()
outputs = models[0].forward(nv12_data)
t1 = time.time()
数据后处理
yolov5_postprocess_info = Yolov5PostProcessInfo_t()
yolov5_postprocess_info.height = h
yolov5_postprocess_info.width = w
org_height, org_width = img_file.shape[0:2]
yolov5_postprocess_info.ori_height = org_height
yolov5_postprocess_info.ori_width = org_width
yolov5_postprocess_info.score_threshold = 0.4
yolov5_postprocess_info.nms_threshold = 0.45
yolov5_postprocess_info.nms_top_k = 20
yolov5_postprocess_info.is_pad_resize = 0
output_tensors = (hbDNNTensor_t * len(models[0].outputs))()
for i in range(len(models[0].outputs)):
output_tensors[i].properties.tensorLayout = get_TensorLayout(outputs[i].properties.layout)
if (len(outputs[i].properties.scale_data) == 0):
output_tensors[i].properties.quantiType = 0
output_tensors[i].sysMem[0].virAddr = ctypes.cast(outputs[i].buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), ctypes.c_void_p)
else:
output_tensors[i].properties.quantiType = 2
output_tensors[i].properties.scale.scaleData = outputs[i].properties.scale_data.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
output_tensors[i].sysMem[0].virAddr = ctypes.cast(outputs[i].buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)), ctypes.c_void_p)
for j in range(len(outputs[i].properties.shape)):
output_tensors[i].properties.validShape.dimensionSize[j] = outputs[i].properties.shape[j]
libpostprocess.Yolov5doProcess(output_tensors[i], ctypes.pointer(yolov5_postprocess_info), i)
result_str = get_Postprocess_result(ctypes.pointer(yolov5_postprocess_info))
result_str = result_str.decode('utf-8')
这里进行YOLOv5模型的后处理信息,并调用外部C库进行后处理操作。首先创建一个Yolov5PostProcessInfo_t实例,该结构体包含后处理所需的各种参数。之后对每个输出张量调用Yolov5doProcess函数进行处理,得到字符串格式的输出。
结果解析并绘制边界框
data = json.loads(result_str[16:])
for result in data:
bbox = result['bbox'] # 矩形框位置信息
score = result['score'] # 得分
id = result['id'] # id
name = result['name'] # 类别名称
cv2.rectangle(img_file, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (0, 255, 0), 2)
font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img_file, f'{name} {score:.2f}', (int(bbox[0]), int(bbox[1]) - 10), font, 0.5, (0, 255, 0), 1)
cv2.imwrite('output_image.jpg', img_file)
最后对字符串进行解析,获取图像中的目标检测结果并绘制到图片并输出。得到结果如下:
- 2025-01-24
-
回复了主题帖:
EEWORLD陪你过大年,新年积分兑换专场来啦~好物多多还有幸运盲盒!
兑的盲盒,极兔说快递找不着了,这咋办
- 2025-01-21
-
回复了主题帖:
《深度学习的数学——使用Python语言》8. 线代进阶之”主成分分析与奇异值分解“
FreddieJohn 发表于 2025-1-21 15:37
谢谢分享
一起学习进步
-
发表了主题帖:
《深度学习的数学——使用Python语言》8. 线代进阶之”主成分分析与奇异值分解“
# 主成分分析
在多变量大数据集的研究与应用中,数据采集和分析的复杂性增加,主要因为许多变量之间存在相关性。孤立地分析每个指标不仅效率低,还可能导致信息损失和错误结论。为了有效利用数据中的信息并减少分析维度,可以采用降维技术,如主成分分析(PCA)。这些方法通过合并相关变量来降低数据维度,同时尽量保留原指标的信息,确保新变量能代表原始数据中的关键信息。降维不仅可以简化模型、去除噪声和不重要的特征,还能显著提升数据处理速度,节省时间和成本。
主成分分析(PCA)是一种广泛使用的数据降维算法,其核心思想是将高维特征映射到低维空间中的新正交特征(即主成分)。PCA通过选择原始数据中方差最大的方向作为第一个坐标轴,然后在与之前坐标轴正交的平面中选择方差最大的方向作为后续坐标轴,依次类推。这样,大部分方差集中在前k个坐标轴上,而后面的坐标轴所含方差几乎为零。因此,可以忽略这些方差极小的坐标轴,仅保留包含绝大部分方差的前k个坐标轴,从而实现数据的有效降维,简化数据结构并提高计算效率。
PCA的工作流程如下:
1. 对数据去均值
2. 计算去均值后的数据的协方差矩阵
3. 计算协方差矩阵的特征值和特征向量
4. 对特征值的绝对值从大到小进行排序
5. 去掉幅度最小的几个特征值和对应的特征向量
6. 用余下的特征向量构造变换矩阵W
7. 利用变换矩阵将原始值变换为新的值,即x‘=Wx
实现代码如下:
1. 导入必要的库
```python
import numpy as np
from sklearn.datasets import load_iris
```
2. 导入iris数据集并去除均值
```
iris = load_iris().data.copy()
labels = load_iris().target.copy()
m = iris.mean(axis=0)
ir = iris - m
```
3. 计算协方差矩阵,特征值和特征向量
```
cv = np.cov(ir, rowvar=False)
val, vec = np.linalg.eig(cv)
```
4. 对特征值进行从大到小排序
```
val = np.abs(val)
idx = np.argsort(val)[::-1]
ex = val[idx] / val.sum()
print("fraction explained: ", ex)
```
5. 提取主成分并构造新的特征
```
w = np.vstack((vec[:,idx[0]],vec[:,idx[1]]))
d = np.zeros((ir.shape[0],2))
for i in range(ir.shape[0]):
d[i,:] = np.dot(w,ir)
```
从第4步的打印结果来看,前2个主成分已经解释了数据集中接近98%的方差,故只需要保留其中的前两个主成分即可。
之后我们绘制出经过变换后的新的数据集如下:
可以看到,不同类别数据之间有较好的区分度,从而使得整个数据集变得更容易处理。
此外,再sklearn.decomposition模块中,可提供了PCA类,能够帮助我们快速实现主成分分析的计算。
```
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
pca.fit(ir)
dd = pca.fit_transform(ir)
```
其中首先制定了所需的成分数,之后调用fit生成了转移矩阵,之后调用fit_transform函数完成特征变换,最终的变量d就是降维后生成的新数据。
# 奇异值分解
奇异值分解(SVD)是一项非常有用的技术。利用SVD,我们可以将任意矩阵分解成为3个具有特殊性质的矩阵相乘形式。在通信中,我们能够基于SVD来设计人工噪声。通式如下:
其中SVD将m*n大小的矩阵A分解为了m*m的正交矩阵U、m*n的对角矩阵以及n*n的正交矩阵V。其中对角矩阵中的元素为奇异值,等于A的转置乘A的正特征值的平方根。
代码实现如下:
```
import numpy as np
from scipy.linalg import svd as SVD
a = np.array([[3,2,2],[2,3,-2]])
u,s,vt = SVD(a)
print(f"u={u}")
print(f"s={s}")
print(f"vt={vt}")
```
运行结果如下:
- 2025-01-18
-
回复了主题帖:
《深度学习的数学——使用Python语言》7. 线代进阶之”向量范数和距离度量“
Jacktang 发表于 2025-1-18 10:14
结果中竟然出现了inf,虽然这并不影响我们的判断
这个inf出现的原因是什么
下面是打印出来的p q和w的值 根据离散情况下的K-L散度计算公式,因为q中出现了0,所以在计算KL(p||q)时,由于分母为零导致出现无穷大inf的情况。
-
回复了主题帖:
RDK X3测评:2.摄像头使用
eew_Eu6WaC 发表于 2025-1-17 16:27
写的真详细,期待这块板子后续更多功能,比如一些AI相关的应用
-
回复了主题帖:
RDK X3测评:2.摄像头使用
SeasonMay 发表于 2025-1-17 15:47
opencv-python的部署效果好不好呢?
暂时还没来得及测试,但官方文档里给出了基于python的它自己的hobot_cv和opencv的性能对比,里面有提到opencv的性能,可以参考一下5.2.4 图像处理加速 | RDK DOC
- 2025-01-16
-
发表了主题帖:
《深度学习的数学——使用Python语言》7. 线代进阶之”向量范数和距离度量“
在深度学习中,范数和向量距离是两个不同的概念。向量范数是一种函数,用于将一个实数或复数向量映射为一个值。虽然范数通常用于度量向量之间的距离,但是同样也有其它的一些表示距离的方式。
# 范数距离
范数是具有“长度”概念的函数。在向量空间内,为所有的向量的赋予非零的增长度或者大小。不同的范数,所求的向量的长度或者大小是不同的。
L1范数是指向量中各个元素绝对值之和,L2范数定义为向量所有元素的平方和的开平方。
L1范数的定义:
L2范数的定义:
在深度学习中,二范数可用于正则化的权值衰减法,从而避免模型的权重变得过大。
此外,在facenet人脸识别模型中,就是将将人脸图片表征为一个128维向量,通过计算两个人脸向量之间的二范数距离,来判断两张人脸图片是否属于同一个人。
下面的代码读取了两张人脸图片,分别获得了128维的输出向量并进行正则化,之后使用L2范数计算它们之间的距离。
```
# Set inputs and inference
image_1 = Image.open("./dataset/facenet/1_001.jpg")
image_1 = image_1.resize((160,160), Image.BICUBIC)
img1 = np.asarray(image_1, np.uint8)
outputs1 = np.array(rknn.inference(data_format='nhwc', inputs=[img1])[0])
outputs1 = preprocessing.normalize(outputs1, norm='l2')
image_2 = Image.open("./dataset/facenet/1_002.jpg")
image_2 = image_2.resize((160,160), Image.BICUBIC)
img2 = np.asarray(image_2, np.uint8)
outputs2 = np.array(rknn.inference(data_format='nhwc', inputs=[img2])[0])
outputs2 = preprocessing.normalize(outputs2, norm='l2')
# Get distance
distance = np.linalg.norm(outputs2 - outputs1, axis=1)
print("distance:", distance)
```
# 马氏距离
当需要度量某个特征向量与多个特征向量之间的距离时,如果直接采用欧式距离,衡量的是两个特征向量之间的直接距离,而没有考虑数据的分布特性。而采用马氏距离,在计算中对协方差进行归一化,则可以规避欧式距离对于数据特征方差不同的风险,从而使所谓的“距离”更加符合数据分布特征以及实际意义。
其中Σ是多维随机变量的协方差矩阵,μ为样本均值向量。马氏距离刻画了x与以μ为均值的某个分布之间的距离。如果协方差矩阵是单位向量,即各维度独立同分布,此时马氏距离就变成了欧氏距离。
直观的解释如上图,虽然Point1和Point2距离样本中心点的距离相同,但是在右图中,可以明显看出来,Point2不属于该分布。
因此,我们可以使用马氏距离来构建一个简单的分类器。比如,给定数据集,判断输入样本与所有类别质心之间的马氏距离,并选择最小距离作为该输入的类别。
下面以sklearn中的乳腺癌数据集来构建基于马氏距离的最近质心分类器,从而进一步加深理解与认识。
代码实现如下:
导入需要的库
```
import numpy as np
from sklearn import datasets
from scipy.spatial.distance import mahalanobis
读取数据集并打乱,取前400个样本作为训练数据并将剩下的样本作为测试数据
bc = datasets.load_breast_cancer()
d = bc.data
l = bc.target
i = np.argsort(np.random.random(len(d)))
d = d
l = l
xtrn, ytrn = d[:400], l[:400]
xtst, ytst = d[400:], l[400:]
```
计算每个类别数据的均值以及训练数据集的协方差矩阵和协方差矩阵的逆
```
i = np.where(ytrn == 0)
m0 = xtrn.mean(axis=0)
i = np.where(ytrn == 1)
m1 = xtrn.mean(axis=0)
S = np.cov(xtrn, rowvar=False)
SI= np.linalg.inv(S)
```
定义函数,对测试数据集进行分类
```
def score(xtst, ytst, m, SI):
nc = 0
for i in range(len(ytst)):
d = np.array([mahalanobis(xtst,m[0],SI),
mahalanobis(xtst,m[1],SI)])
c = np.argmin(d)
if (c == ytst):
nc += 1
return nc / len(ytst)
```
分别计算马氏距离和欧氏距离的结果得分
```
mscore = score(xtst, ytst, [m0,m1], SI)
escore = score(xtst, ytst, [m0,m1], np.identity(30))
print("Mahalanobis score = %0.4f" % mscore)
print("Euclidean score = %0.4f" % escore)
```
运行结果如下:
可以看到,马氏距离分类的效果比欧氏距离的效果有所提升。
# K-L散度
K-L散度又称相对熵,用于衡量两个概率分布的相似程度。若K-L散度越小,则说明两个概率分布越相似。
我们设定两个概率分布分别为P和Q,则连续和离散情况下的K-L散度计算公式分别为
其中log为以2为底的对数,在scipy.special中使用rel_entr函数来实现K-L散度的计算,但是它使用的是自然对数而不是以2为底的对数。此外,K-L散度不满足对称性,并非数学意义上的距离度量。
下面我们通过一个实验来加深对于K-L散度的理解。
代码实现如下:
首先导入使用到的库
```
import numpy as np
from scipy.special import rel_entr
import matplotlib.pylab as plt
```
分别生成服从均匀分布和两组二项分布B(12,0.4)以及B(12,0.9)。
```
N = 1000000
p = np.random.randint(0,13,size=N)
p = np.bincount(p)
p = p / p.sum()
q = np.random.binomial(12,0.9,size=N)
q = np.bincount(q)
q = q / q.sum()
w = np.random.binomial(12,0.4,size=N)
w = np.bincount(w)
w = w / w.sum()
```
使用rel_entr函数计算两组二项分布与均匀分布的K-L散度,判断哪个二项分布更接近于均匀分布
```
print(rel_entr(q,p).sum())
print(rel_entr(w,p).sum())
```
输出结果如下:
可以看到,w与p的K-L散度更小,说明w与p的概率分布更为相似。事实是否如此呢?我们来看一下三个不同的离散概率分布的图像。
```
plt.bar(np.arange(13),p,0.333,hatch="///",edgecolor='k')
plt.bar(np.arange(13)+0.333,q,0.333,hatch="---",edgecolor='k')
plt.bar(np.arange(13)+0.666,w,0.333,hatch="\\\\",edgecolor='k')
plt.xlabel("Value")
plt.ylabel("Proportion")
plt.tight_layout(pad=0,h_pad=0,w_pad=0)
plt.savefig("kl_divergence.png", dpi=300)
plt.show()
```
其中蓝色为p,橙色为q,绿色为w。可以看到与q相比,p确实更像w,说明我们通过K-L散度的判断是正确的。
此外,既然前面提到了K-L散度是非对称的,那么我们在计算K-L散度时将输入变量换一下位置会产生什么样的后果呢?我们将计算w与p的散度时的输入换一下顺序
```
print(rel_entr(q,p).sum())
print(rel_entr(p,w).sum())
```
得到结果如下:
可以看到第二项反而比第一项更大了,这显然不是我们期望的结果。那么在实际使用中,应该如何确定输入变量的顺序呢?
为了解决这个问题,我们首先需要理解K-L散度的意义,KL(P||Q)衡量的是分布P相对于Q的信息损失,即当Q被用作P的近似时,描述P所需的额外信息量。如果将P设为目标分布、Q设为近似分布,则KL(P||Q)越小说明用Q描述P所需的额外信息量越小,即P和Q越接近。相反KL(Q||P)越小,则说明用P描述Q所需的额外信息量越小。因此,在上面比较q和w与p之间的距离时,我们需要固定输入中p的位置,这样得到的数值才具有可比性。
然而,若我们将p都作为第一个输入变量,下面又出现了新的问题
```
print(rel_entr(p,q).sum())
print(rel_entr(p,w).sum())
```
输出结果如下:
可以看到,结果中竟然出现了inf,虽然这并不影响我们的判断,但是inf在计算中会给我们带来极大的不便,如何避免这个问题呢?下面给出了解答。
- 2025-01-14
-
回复了主题帖:
RDK X3测评:2.摄像头使用
dirty 发表于 2025-1-14 21:14
摄像头驱动功能验证了,后面可以做模型部署 目标检测人脸识别这些AI了哈,期待后续
-
回复了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--1.开箱与准备
dirty 发表于 2025-1-14 20:24
看芯片手册是没有专门的NPU,结合ST的工具链,跑的边缘AI,不太适合跑大模型
期待后续的测评分享
-
回复了主题帖:
RDK X3测评:2.摄像头使用
摄像头接线方式如图