我自己的原文哦~ https://blog.51cto.com/whaosoft/12663254
#Motion Plan
代码 github.com/liangwq/robot_motion_planing
轨迹约束中的软硬约束
前面的几篇文章已经介绍了,轨迹约束的本质就是在做带约束的轨迹拟合。输入就是waypoint点list,约束条件有两种硬约束和软约束。所谓硬约束对应到数学形式就是代价函数,硬约束对应的就是最优化秋季的约束条件部分。对应到物理意义就是,为了获得机器人可行走的安全的轨迹有:
- 把轨迹通过代价函数推离障碍物的方式
- 给出障碍物之间的可行走凸包走廊,通过硬约束让机器人轨迹必须在凸包走廊行走
上图展示的是软硬约束下Bezier曲线拟合的求解的数学框架,以及如何把各种的约束条件转成数学求解的代价函数(软约束)或者是求解的约束条件(软约束)。image.png
上面是对常用的代价函数约束的几种表示方式的举例。image.png
Bezier曲线拟合轨迹
前面已经一篇文章介绍过贝赛尔曲线拟合的各种优点:
- 端点插值。贝塞尔曲线始终从第一个控制点开始,结束于最后一个控制点,并且不会经过任何其他控制点。
- 凸包。贝塞尔曲线 ( ) 由一组控制点 完全限制在由所有这些控制点定义的凸包内。
- 速度曲线。贝塞尔曲线 ( ) 的导数曲线 ′( ) 被称为速度曲线,它也是一个由控制点定义的贝塞尔曲线,其中控制点为 ∙ ( +1− ),其中 是阶数。
- 固定时间间隔。贝塞尔曲线始终在 [0,1] 上定义。
Fig1.一段轨迹用bezier曲线拟合image.png
上面的两个表达式对应的代码实现如下:
def bernstein_poly(n, i, t):
"""
Bernstein polynom.
:param n: (int) polynom degree
:param i: (int)
:param t: (float)
:return: (float)
"""
return scipy.special.comb(n, i) * t ** i * (1 - t) ** (n - i)
def bezier(t, control_points):
"""
Return one point on the bezier curve.
:param t: (float) number in [0, 1]
:param control_points: (numpy array)
:return: (numpy array) Coordinates of the point
"""
n = len(control_points) - 1
return np.sum([bernstein_poly(n, i, t) * control_points[i] for i in range(n + 1)], axis=0)
要用Bezier曲线来表示一段曲线,上面已经给出了表达式和代码实现,现在缺的就是给定控制点,把控制点带进来用Bezier曲线表达式来算出给定终点和结束点中需要画的点坐标。下面代码给出了4个控制点、6个控制点的Bezier曲线实现;其中为了画曲线需要算170个线上点。代码如下:
def calc_4points_bezier_path(sx, sy, syaw, ex, ey, eyaw, offset):
"""
Compute control points and path given start and end position.
:param sx: (float) x-coordinate of the starting point
:param sy: (float) y-coordinate of the starting point
:param syaw: (float) yaw angle at start
:param ex: (float) x-coordinate of the ending point
:param ey: (float) y-coordinate of the ending point
:param eyaw: (float) yaw angle at the end
:param offset: (float)
:return: (numpy array, numpy array)
"""
dist = np.hypot(sx - ex, sy - ey) / offset
control_points = np.array(
[[sx, sy],
[sx + dist * np.cos(syaw), sy + dist * np.sin(syaw)],
[ex - dist * np.cos(eyaw), ey - dist * np.sin(eyaw)],
[ex, ey]])
path = calc_bezier_path(control_points, n_points=170)
return path, control_points
def calc_6points_bezier_path(sx, sy, syaw, ex, ey, eyaw, offset):
"""
Compute control points and path given start and end position.
:param sx: (float) x-coordinate of the starting point
:param sy: (float) y-coordinate of the starting point
:param syaw: (float) yaw angle at start
:param ex: (float) x-coordinate of the ending point
:param ey: (float) y-coordinate of the ending point
:param eyaw: (float) yaw angle at the end
:param offset: (float)
:return: (numpy array, numpy array)
"""
dist = np.hypot(sx - ex, sy - ey) * offset
control_points = np.array(
[[sx, sy],
[sx + 0.25 * dist * np.cos(syaw), sy + 0.25 * dist * np.sin(syaw)],
[sx + 0.40 * dist * np.cos(syaw), sy + 0.40 * dist * np.sin(syaw)],
[ex - 0.40 * dist * np.cos(eyaw), ey - 0.40 * dist * np.sin(eyaw)],
[ex - 0.25 * dist * np.cos(eyaw), ey - 0.25 * dist * np.sin(eyaw)],
[ex, ey]])
path = calc_bezier_path(control_points, n_points=170)
return path, control_points
def calc_bezier_path(control_points, n_points=100):
"""
Compute bezier path (trajectory) given control points.
:param control_points: (numpy array)
:param n_points: (int) number of points in the trajectory
:return: (numpy array)
"""
traj = []
for t in np.linspace(0, 1, n_points):
traj.append(bezier(t, control_points))
return np.array(traj)
有了一段Bezier曲线的拟合方式,接下来要做的就是如何生成多段Bezier曲线合成一条轨迹;并且是需要可以通过代价函数方式(软约束)+必须经过指定点+制定点衔接要连续(硬约束)来生成一条光滑的轨迹曲线。
Fig2.无障碍物,带bondary轨迹做了smooth优化Figure_1.png
多段Bezier曲线生成代码如下,其实原理很简单,给定多个waypoint点,每相邻两个wayponit生成一段Bezizer曲线,代码如下:
# Bezier path one as per the approach suggested in
# https://users.soe.ucsc.edu/~elkaim/Documents/camera_WCECS2008_IEEE_ICIAR_58.pdf
def cubic_bezier_path(self, ax, ay):
dyaw, _ = self.calc_yaw_curvature(ax, ay)
cx = []
cy = []
ayaw = dyaw.copy()
for n in range(1, len(ax)-1):
yaw = 0.5*(dyaw[n] + dyaw[n-1])
ayaw[n] = yaw
last_ax = ax[0]
last_ay = ay[0]
last_ayaw = ayaw[0]
# for n waypoints, there are n-1 bezier curves
for i in range(len(ax)-1):
path, ctr_points = calc_4points_bezier_path(last_ax, last_ay, ayaw[i], ax[i+1], ay[i+1], ayaw[i+1], 2.0)
cx = np.concatenate((cx, path.T[0][:-2]))
cy = np.concatenate((cy, path.T[1][:-2]))
cyaw, k = self.calc_yaw_curvature(cx, cy)
last_ax = path.T[0][-1]
last_ay = path.T[1][-1]
return cx, cy
代价函数计算包括:曲率代价 + 偏差代价 + 距离代价 + 连续性代价,同时还有边界条件,轨迹必须在tube内的不等式约束,以及问题优化求解。具体代码实现如下:
# Objective function of cost to be minimized
def cubic_objective_func(self, deviation):
ax = self.waypoints.x.copy()
ay = self.waypoints.y.copy()
for n in range(0, len(deviation)):
ax[n+1] -= deviation[n]*np.sin(self.waypoints.yaw[n+1])
ay[n+1] += deviation[n]*np.cos(self.waypoints.yaw[n+1])
bx, by = self.cubic_bezier_path(ax, ay)
yaw, k = self.calc_yaw_curvature(bx, by)
# cost of curvature continuity
t = np.zeros((len(k)))
dk = self.calc_d(t, k)
absolute_dk = np.absolute(dk)
continuity_cost = 10.0 * np.mean(absolute_dk)
# curvature cost
absolute_k = np.absolute(k)
curvature_cost = 14.0 * np.mean(absolute_k)
# cost of deviation from input waypoints
absolute_dev = np.absolute(deviation)
deviation_cost = 1.0 * np.mean(absolute_dev)
distance_cost = 0.5 * self.calc_path_dist(bx, by)
return curvature_cost + deviation_cost + distance_cost + continuity_cost
# Minimize objective function using scipy optimize minimize
def optimize_min_cubic(self):
print("Attempting optimization minima")
initial_guess = [0, 0, 0, 0, 0]
bnds = ((-self.bound, self.bound), (-self.bound, self.bound), (-self.bound, self.bound), (-self.bound, self.bound), (-self.bound, self.bound))
result = optimize.minimize(self.cubic_objective_func, initial_guess, bounds=bnds)
ax = self.waypoints.x.copy()
ay = self.waypoints.y.copy()
if result.success:
print("optimized true")
deviation = result.x
for n in range(0, len(deviation)):
ax[n+1] -= deviation[n]*np.sin(self.waypoints.yaw[n+1])
ay[n+1] += deviation[n]*np.cos(self.waypoints.yaw[n+1])
x, y = self.cubic_bezier_path(ax, ay)
yaw, k = self.calc_yaw_curvature(x, y)
self.optimized_path = Path(x, y, yaw, k)
else:
print("optimization failure, defaulting")
exit()
带障碍物的Bezier曲线轨迹生
image.png
带有障碍物的场景,通过代价函数让生成的曲线远离障碍物。从而得到一条可以安全行走的轨迹,下面是具体的代码实现。optimizer_k中lambda函数f就是在求解轨迹在经过障碍物附近时候的代价,penalty1、penalty2就是在求曲线经过障碍物附近的具体代价值;
b.arc_len(granuality=10)+B.arc_len(granuality=10)+m_k + penalty1 + penalty2就是轨迹的整体代价。for循环部分用scipy的optimize的minimize来求解轨迹。
def optimizer_k(cd, k, path, i, obs, curve_penalty_multiplier, curve_penalty_divider, curve_penalty_obst):
"""Bezier curve optimizer that optimizes the curvature and path length by changing the distance of p1 and p2 from
points p0 and p3, respectively. """
p_tmp = copy.deepcopy(path)
if i+3 > len(path)-1:
b = CubicBezier()
b.p0 = p_tmp[i]
x, y = calc_p1(p_tmp[i], p_tmp[i + 1], p_tmp[i - 1], i, cd[0])
b.p1 = Point(x, y)
x, y = calc_p2(p_tmp[i-1], p_tmp[i + 0], p_tmp[i + 1], i, cd[1])
b.p2 = Point(x, y)
b.p3 = p_tmp[i + 1]
B = CubicBezier()
else:
b = CubicBezier()
b.p0 = p_tmp[i]
x, y = calc_p1(p_tmp[i],p_tmp[i+1],p_tmp[i-1], i, cd[0])
b.p1 = Point(x, y)
x, y = calc_p2(p_tmp[i],p_tmp[i+1],p_tmp[i+2], i, cd[1])
b.p2 = Point(x, y)
b.p3 = p_tmp[i + 1]
B = CubicBezier()
B.p0 = p_tmp[i]
x, y = calc_p1(p_tmp[i+1], p_tmp[i + 2], p_tmp[i], i, 10)
B.p1 = Point(x, y)
x, y = calc_p2(p_tmp[i+1], p_tmp[i + 2], p_tmp[i + 3], i, 10)
B.p2 = Point(x, y)
B.p3 = p_tmp[i + 1]
m_k = b.max_k()
if m_k>k:
m_k= m_k*curve_penalty_multiplier
else:
m_k = m_k/curve_penalty_divider
f = lambda x, y: max(math.sqrt((x[0] - y[0].x) ** 2 + (x[1] - y[0].y) ** 2) * curve_penalty_obst, 10) if math.sqrt(
(x[0] - y[0].x) ** 2 + (x[1] - y[0].y) ** 2) < y[1] else 0
b_t = b.calc_curve(granuality=10)
b_t = zip(b_t[0],b_t[1])
B_t = B.calc_curve(granuality=10)
B_t = zip(B_t[0], B_t[1])
penalty1 = 0
penalty2 = 0
for o in obs:
for t in b_t:
penalty1 = max(penalty1,f(t,o))
for t in B_t:
penalty2 = max(penalty2,f(t,o))
return b.arc_len(granuality=10)+B.arc_len(granuality=10)+m_k + penalty1 + penalty2
# Optimize the initial path for n_path_opt cycles
for m in range(n_path_opt):
if m%2:
for i in range(1,len(path)-1):
x0 = [0.0, 0.0]
bounds = Bounds([-1, -1], [1, 1])
res = minimize(optimizer_p, x0, args=(path, i, obs, path_penalty), method='TNC', tol=1e-7, bounds=bounds)
x, y = res.x
path[i].x += x
path[i].y += y
else:
for i in range(len(path)-1,1):
x0 = [0.0, 0.0]
bounds = Bounds([-1, -1], [1, 1])
res = minimize(optimizer_p, x0, args=(path, i, obs, path_penalty), method='TNC', tol=1e-7, bounds=bounds)
x, y = res.x
path[i].x += x
path[i].y += y
带飞行走廊的Bezier轨迹生成
得益于贝赛尔曲线拟合的优势,如果我们可以让机器人可行走的轨迹转成多个有重叠区域的凸多面体,那么轨迹完全位于飞行走廊内。
image.png
- 飞行走廊由凸多边形组成。
- 每个立方体对应于一段贝塞尔曲线。
- 此曲线的控制点被强制限制在多边形内部。
- 轨迹完全位于所有点的凸包内。
如何通过把障碍物地图生成可行凸包走廊
生成凸包走廊的方法目前有以下三大类的方法:
平行凸簇膨胀方法
从栅格地图出发,利用最小凸集生成算法,完成凸多面体的生成。其算法的思想是首先获得一个凸集,再沿着凸集的表面进行扩张,扩张之后再进行凸集检测,判断新扩张的集合是否保持为凸。一直扩张到不能再扩张为止,再提取凸集的边缘点,利用快速凸集生成算法,生成凸多面体。该算法的好处在于可以利用这种扩张的思路,将安全的多面体的体积尽可能的充满整个空间,因此获得的安全通道更大。但其也具有一定的缺点,就是计算量比较大,计算所需要的时间比较长,为了解决这个问题,在该文章中,又提出了采用GPU加速的方法,来加速计算。
基于凸分解的安全通道生成
基于凸分解的安全通道生成方法由四个步骤完成安全通道的生成,分别为:找到椭球、找到多面体、边界框、收缩。
半定规划的迭代区域膨胀
为了获取多面体,这个方法首先构造一个初始椭球,由一个以选定点为中心的单位球组成。然后,遍历障碍物,为每个障碍物生成一个超平面,该超平面与障碍物相切并将其与椭球分开。再次,这些超平面定义了一组线性约束,它们的交集是一个多面体。然后,可以在那个多面体中找到一个最大的椭球,使用这个椭球来定义一组新的分离超平面,从而定义一个新的多面体。选择生成分离超平面的方法,这样椭圆体的体积在迭代之间永远不会减少。可以重复这个过程,直到椭圆体的增长率低于某个阈值,此时我们返回多面体和内接椭圆体。这个方法具有迭代的思想,并且具有收敛判断的标准,算法的收敛快慢和初始椭球具有很大的关系。
Fig3.半定规划的迭代区域膨胀。每一行即为一次迭代操作,直到椭圆体的增长率低于阈值。image.png
这篇文章介绍的是“半定规划的迭代区域膨胀”方法,具体代码实现如下:
# 根据输入路径对空间进行凸分解
def decomp(self, line_points: list[np.array], obs_points: list[np.array], visualize=True):
# 最终结果
decomp_polygons = list()
# 构建输入障碍物点的kdtree
obs_kdtree = KDTree(obs_points)
# 进行空间分解
for i in range(len(line_points) - 1):
# 得到当前线段
pf, pr = line_points[i], line_points[i + 1]
print(pf)
print(pr)
# 构建初始多面体
init_polygon = self.initPolygon(pf, pr)
print(init_polygon.getInterPoints())
print(init_polygon.getVerticals())
# 过滤障碍物点
candidate_obs_point_indexes = obs_kdtree.query_ball_point((pf + pr) / 2, np.linalg.norm([np.linalg.norm(pr - pf) / 2 + self.consider_range_, self.consider_range_]))
local_obs_points = list()
for index in candidate_obs_point_indexes:
if init_polygon.inside(obs_points[index]):
local_obs_points.append(obs_points[index])
# 得到初始椭圆
ellipse = self.findEllipse(pf, pr, local_obs_points)
# 根据初始椭圆构建多面体
polygon = self.findPolygon(ellipse, init_polygon, local_obs_points)
# 进行保存
decomp_polygons.append(polygon)
if visualize:
# 进行可视化
plt.figure()
# 绘制路径段
plt.plot([pf[1], pr[1]], [pf[0], pr[0]], color="red")
# 绘制初始多面体
verticals = init_polygon.getVerticals()
# 绘制多面体顶点
plt.plot([v[1] for v in verticals] + [verticals[0][1]], [v[0] for v in verticals] + [verticals[0][0]], color="blue", linestyle="--")
# 绘制障碍物点
plt.scatter([p[1] for p in local_obs_points], [p[0] for p in local_obs_points], marker="o")
# 绘制椭圆
ellipse_x, ellipse_y = list(), list()
for theta in np.linspace(-np.pi, np.pi, 1000):
raw_point = np.array([np.cos(theta), np.sin(theta)])
ellipse_point = np.dot(ellipse.C_, raw_point) + ellipse.d_
ellipse_x.append(ellipse_point[0])
ellipse_y.append(ellipse_point[1])
plt.plot(ellipse_y, ellipse_x, color="orange")
# 绘制最终多面体
# 得到多面体顶点
verticals = polygon.getVerticals()
# 绘制多面体顶点
plt.plot([v[1] for v in verticals] + [verticals[0][1]], [v[0] for v in verticals] + [verticals[0][0]], color="green")
plt.show()
return decomp_polygons
# 构建初始多面体
def initPolygon(self, pf: np.array, pr: np.array) -> Polygon:
# 记录多面体的平面
polygon_planes = list()
# 得到线段方向向量
dire = self.normalize(pr - pf)
# 得到线段法向量
dire_h = np.array([dire[1], -dire[0]])
# 得到平行范围
p_1 = pf + self.consider_range_ * dire_h
p_2 = pf - self.consider_range_ * dire_h
polygon_planes.append(Hyperplane(dire_h, p_1))
polygon_planes.append(Hyperplane(-dire_h, p_2))
# 得到垂直范围
p_3 = pr + self.consider_range_ * dire
p_4 = pf - self.consider_range_ * dire
polygon_planes.append(Hyperplane(dire, p_3))
polygon_planes.append(Hyperplane(-dire, p_4))
# 构建多面体
polygon = Polygon(polygon_planes)
return polygon
# 得到初始椭圆
def findEllipse(self, pf: np.array, pr: np.array, obs_points: list[np.array]) -> Ellipse:
# 计算长轴
long_axis_value = np.linalg.norm(pr - pf) / 2
axes = np.array([long_axis_value, long_axis_value])
# 计算旋转
rotation = self.vec2Rotation(pr - pf)
# 计算初始椭圆
C = np.dot(rotation, np.dot(np.array([[axes[0], 0], [0, axes[1]]]), np.transpose(rotation)))
d = (pr + pf) / 2
ellipse = Ellipse(C, d)
# 得到椭圆内的障碍物点
inside_obs_points = ellipse.insidePoints(obs_points)
# 对椭圆进行调整,使得全部障碍物点都在椭圆外
while inside_obs_points:
# 得到与椭圆距离最近的点
closest_obs_point = ellipse.closestPoint(inside_obs_points)
# 将最近点转到椭圆坐标系下
closest_obs_point = np.dot(np.transpose(rotation), closest_obs_point - ellipse.d_)
# 根据最近点,在椭圆长轴不变的情况下对短轴进行改变,使得,障碍物点在椭圆上
if Compare.small(closest_obs_point[0], axes[0]):
axes[1] = np.abs(closest_obs_point[1]) / np.sqrt(1 - (closest_obs_point[0] / axes[0]) ** 2)
# 更新椭圆
ellipse.C_ = np.dot(rotation, np.dot(np.array([[axes[0], 0], [0, axes[1]]]), np.transpose(rotation)))
# 更新椭圆内部障碍物
inside_obs_points = ellipse.insidePoints(inside_obs_points, include_bound=False)
return ellipse
# 进行多面体的构建
def findPolygon(self, ellipse: Ellipse, init_polygon: Polygon, obs_points: list[np.array]) -> Polygon:
# 多面体由多个超平面构成
polygon_planes = copy.deepcopy(init_polygon.hyper_planes_)
# 初始化范围超平面
remain_obs_points = obs_points
while remain_obs_points:
# 得到与椭圆最近障碍物
closest_point = ellipse.closestPoint(remain_obs_points)
# 计算该处的切平面的法向量
norm_vector = np.dot(np.linalg.inv(ellipse.C_), np.dot(np.linalg.inv(ellipse.C_), (closest_point - ellipse.d_)))
norm_vector = self.normalize(norm_vector)
# 构建平面
hyper_plane = Hyperplane(norm_vector, closest_point)
# 保存到多面体平面中
polygon_planes.append(hyper_plane)
# 去除切平面外部的障碍物
new_remain_obs_points = list()
for point in remain_obs_points:
if Compare.small(hyper_plane.signDist(point), 0):
new_remain_obs_points.append(point)
remain_obs_points = new_remain_obs_points
polygon = Polygon(polygon_planes)
return polygon
image.png
上面图是给定16个障碍物点,必经6个路径点后得到的凸包可行走廊,具体代码如下:
def main():
# 路径点
line_points = [np.array([-1.5, 0.0]), np.array([0.0, 0.8]), np.array([1.5, 0.3]), np.array([5, 0.6]), np.array([6, 1.2]), np.array([7.6, 2.2])]
# 障碍物点
obs_points = [
np.array([4, 2.0]),
np.array([6, 3.0]),
np.array([2, 1.5]),
np.array([0, 1]),
np.array([1, 0]),
np.array([1.8, 0]),
np.array([3.8, 2]),
np.array([0.5, 1.2]),
np.array([4.3, 0]),
np.array([8, 0.9]),
np.array([2.8, -0.3]),
np.array([6, -0.9]),
np.array([-0.5, -0.5]),
np.array([-0.75 ,-0.5]),
np.array([-1, -0.5]),
np.array([-1, 0.8])
]
convex_decomp = ConvexDecomp(2)
decomp_polygons = convex_decomp.decomp(line_points, obs_points, False)
#convex_decomp.decomp(line_points, obs_points,False)
plt.figure()
# 绘制障碍物点
plt.scatter([p[0] for p in obs_points], [p[1] for p in obs_points], marker="o")
# 绘制边界
for polygon in decomp_polygons:
verticals = polygon.getVerticals()
# 绘制多面体顶点
plt.plot([v[0] for v in verticals] + [verticals[0][0]], [v[1] for v in verticals] + [verticals[0][1]], color="green")
#plt.plot(x_samples, y_samples)
plt.show()
带凸包走廊求解
带凸包走廊的光滑轨迹生成。前面已经求解得到了可行的凸包走廊,这部分可以做为硬约束作为最优化求解的不等式条件。要求的光滑路径和必须经过点的点,这部分可以把必须经过点作为等式约束,光滑路径可以通过代价函数来实现。这样就可以把带软硬约束的轨迹生成框架各种技能点都用上了。
image.png
下面看具体代码实现:
# 进行优化
def optimize(self, start_state: np.array, end_state: np.array, line_points: list[np.array], polygons: list[Polygon]):
assert(len(line_points) == len(polygons) + 1)
# 得到分段数量
segment_num = len(polygons)
assert(segment_num >= 1)
# 计算初始时间分配
time_allocations = list()
for i in range(segment_num):
time_allocations.append(np.linalg.norm(line_points[i+1] - line_points[i]) / self.vel_max_)
# 进行优化迭代
max_inter = 10
cur_iter = 0
while cur_iter < max_inter:
# 进行轨迹优化
piece_wise_trajectory = self.optimizeIter(start_state, end_state, polygons, time_allocations, segment_num)
# 对优化轨迹进行时间调整,以保证轨迹满足运动上限约束
cur_iter += 1
# 计算每一段轨迹的最大速度,最大加速度,最大jerk
condition_fit = True
for n in range(segment_num):
# 得到最大速度,最大加速度,最大jerk
t_samples = np.linspace(0, time_allocations[n], 100)
v_max, a_max, j_max = self.vel_max_, self.acc_max_, self.jerk_max_
for t_sample in t_samples:
v_max = max(v_max, np.abs(piece_wise_trajectory.trajectory_segments_[n][0].derivative(t_sample)), np.abs(piece_wise_trajectory.trajectory_segments_[n][1].derivative(t_sample)))
a_max = max(a_max, np.abs(piece_wise_trajectory.trajectory_segments_[n][0].secondOrderDerivative(t_sample)), np.abs(piece_wise_trajectory.trajectory_segments_[n][1].secondOrderDerivative(t_sample)))
j_max = max(j_max, np.abs(piece_wise_trajectory.trajectory_segments_[n][0].thirdOrderDerivative(t_sample)), np.abs(piece_wise_trajectory.trajectory_segments_[n][1].thirdOrderDerivative(t_sample)))
# 判断是否满足约束条件
if Compare.large(v_max, self.vel_max_) or Compare.large(a_max, self.acc_max_) or Compare.large(j_max, self.jerk_max_):
ratio = max(1, v_max / self.vel_max_, (a_max / self.acc_max_)**0.5, (j_max / self.jerk_max_)**(1/3))
time_allocations[n] = ratio * time_allocations[n]
condition_fit = False
if condition_fit:
break
return piece_wise_trajectory
# 优化迭代
def optimizeIter(self, start_state: np.array, end_state: np.array, polygons: list[Polygon], time_allocations: list, segment_num):
# 构建目标函数 inter (jerk)^2
inte_jerk_square = np.array([
[720.0, -1800.0, 1200.0, 0.0, 0.0, -120.0],
[-1800.0, 4800.0, -3600.0, 0.0, 600.0, 0.0],
[1200.0, -3600.0, 3600.0, -1200.0, 0.0, 0.0],
[0.0, 0.0, -1200.0, 3600.0, -3600.0, 1200.0],
[0.0, 600.0, 0.0, -3600.0, 4800.0, -1800.0],
[-120.0, 0.0, 0.0, 1200.0, -1800.0, 720.0]
])
# 二次项系数
P = np.zeros((self.dim_ * segment_num * self.freedom_, self.dim_ * segment_num * self.freedom_))
for sigma in range(self.dim_):
for n in range(segment_num):
for i in range(self.freedom_):
for j in range(self.freedom_):
index_i = sigma * segment_num * self.freedom_ + n * self.freedom_ + i
index_j = sigma * segment_num * self.freedom_ + n * self.freedom_ + j
P[index_i][index_j] = inte_jerk_square[i][j] / (time_allocations[n] ** 5)
P = P * 2
P = sparse.csc_matrix(P)
# 一次项系数
q = np.zeros((self.dim_ * segment_num * self.freedom_,))
# 构建约束条件
equality_constraints_num = 5 * self.dim_ + 3 * (segment_num - 1) * self.dim_
inequality_constraints_num = 0
for polygon in polygons:
inequality_constraints_num += self.freedom_ * len(polygon.hyper_planes_)
A = np.zeros((equality_constraints_num + inequality_constraints_num, self.dim_ * segment_num * self.freedom_))
lb = -float("inf") * np.ones((equality_constraints_num + inequality_constraints_num,))
ub = float("inf") * np.ones((equality_constraints_num + inequality_constraints_num,))
# 构建等式约束条件(起点位置、速度、加速度;终点位置、速度;连接处的零、一、二阶导数)
# 起点x位置
A[0][0] = 1
lb[0] = start_state[0]
ub[0] = start_state[0]
# 起点y位置
A[1][segment_num * self.freedom_] = 1
lb[1] = start_state[1]
ub[1] = start_state[1]
# 起点x速度
A[2][0] = -5 / time_allocations[0]
A[2][1] = 5 / time_allocations[0]
lb[2] = start_state[2]
ub[2] = start_state[2]
# 起点y速度
A[3][segment_num * self.freedom_] = -5 / time_allocations[0]
A[3][segment_num * self.freedom_ + 1] = 5 / time_allocations[0]
lb[3] = start_state[3]
ub[3] = start_state[3]
# 起点x加速度
A[4][0] = 20 / time_allocations[0]**2
A[4][1] = -40 / time_allocations[0]**2
A[4][2] = 20 / time_allocations[0]**2
lb[4] = start_state[4]
ub[4] = start_state[4]
# 起点y加速度
A[5][segment_num * self.freedom_] = 20 / time_allocations[0]**2
A[5][segment_num * self.freedom_ + 1] = -40 / time_allocations[0]**2
A[5][segment_num * self.freedom_ + 2] = 20 / time_allocations[0]**2
lb[5] = start_state[5]
ub[5] = start_state[5]
# 终点x位置
A[6][segment_num * self.freedom_ - 1] = 1
lb[6] = end_state[0]
ub[6] = end_state[0]
# 终点y位置
A[7][self.dim_ * segment_num * self.freedom_ - 1] = 1
lb[7] = end_state[1]
ub[7] = end_state[1]
# 终点x速度
A[8][segment_num * self.freedom_ - 1] = 5 / time_allocations[-1]
A[8][segment_num * self.freedom_ - 2] = -5 / time_allocations[-1]
lb[8] = end_state[2]
ub[8] = end_state[2]
# 终点y速度
A[9][self.dim_ * segment_num * self.freedom_ - 1] = 5 / time_allocations[-1]
A[9][self.dim_ * segment_num * self.freedom_ - 2] = -5 / time_allocations[-1]
lb[9] = end_state[3]
ub[9] = end_state[3]
# 连接处的零阶导数相等
constraints_index = 10
for sigma in range(self.dim_):
for n in range(segment_num - 1):
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 1] = 1
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_] = -1
lb[constraints_index] = 0
ub[constraints_index] = 0
constraints_index += 1
# 连接处的一阶导数相等
for sigma in range(self.dim_):
for n in range(segment_num - 1):
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 1] = 5 / time_allocations[n]
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 2] = -5 / time_allocations[n]
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_] = 5 / time_allocations[n + 1]
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_ + 1] = -5 / time_allocations[n + 1]
lb[constraints_index] = 0
ub[constraints_index] = 0
constraints_index += 1
# 连接处的二阶导数相等
for sigma in range(self.dim_):
for n in range(segment_num - 1):
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 1] = 20 / time_allocations[n]**2
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 2] = -40 / time_allocations[n]**2
A[constraints_index][sigma * segment_num * self.freedom_ + n * self.freedom_ + self.freedom_ - 3] = 20 / time_allocations[n]**2
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_] = -20 / time_allocations[n + 1]**2
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_ + 1] = 40 / time_allocations[n + 1]**2
A[constraints_index][sigma * segment_num * self.freedom_ + (n+1) * self.freedom_ + 2] = -20 / time_allocations[n + 1]**2
lb[constraints_index] = 0
ub[constraints_index] = 0
constraints_index += 1
# 构建不等式约束条件
for n in range(segment_num):
for k in range(self.freedom_):
for hyper_plane in polygons[n].hyper_planes_:
A[constraints_index][n * self.freedom_ + k] = hyper_plane.n_[0]
A[constraints_index][segment_num * self.freedom_ + n * self.freedom_ + k] = hyper_plane.n_[1]
ub[constraints_index] = np.dot(hyper_plane.n_, hyper_plane.d_)
constraints_index += 1
assert(constraints_index == equality_constraints_num + inequality_constraints_num)
A = sparse.csc_matrix(A)
# 进行qp求解
prob = osqp.OSQP()
prob.setup(P, q, A, lb, ub, warm_start=True)
res = prob.solve()
if res.info.status != "solved":
raise ValueError("OSQP did not solve the problem!")
# 根据参数进行轨迹解析
trajectory_x_params, trajectory_y_params = list(), list()
for n in range(segment_num):
trajectory_x_params.append(res.x[self.freedom_ * n: self.freedom_ * (n+1)])
trajectory_y_params.append(res.x[segment_num * self.freedom_ + self.freedom_ * n: segment_num * self.freedom_ + self.freedom_ * (n+1)])
piece_wise_trajectory = PieceWiseTrajectory(trajectory_x_params, trajectory_y_params, time_allocations)
return piece_wise_trajectory
小结:
这篇文章介绍了带软硬约束的轨迹优化算法框架。第一部份介绍了软硬约束对应到最优求解问题数学上如何表示。第二部份介绍了贝赛尔曲线的代码实现,给出了具体的代码实现和讲解;并针对没有障碍物场景只给定waypoint点,生成光滑的Bezier轨迹的朴素求解代码实现。第三部份给出了带障碍物情况下如何做最优化求解,如何通过代价函数的方式来给轨迹施加推力让轨迹远离障碍物的代码实现。第四部分是一个综合性的例子,把软硬约束最优轨迹生成的求解框架做了一个综合呈现。详细的介绍了如何利用障碍物地图生成最大可行区域的凸包走廊,如何利用Bezier曲线的特性给定凸包两点生成路径一定在凸包中;以及如何利用代价行数来保证轨迹的光滑性、安全性,通过等式、不等式约束实现轨迹必须经过哪些点,某个点的运动状态如何。
这一系列的文章已经进入结尾的阶段,后面会简单介绍在碰到移动的物体时候单机器人如何处理;以及在多个机器人运行环境如何协同,最后会给出一个Motion Planning的综合实现例子讲解实际环境数据输入、前端规划、后端轨迹生成。至于定位和感知部分的内容后面可以根据情况而定是否在开一个新的系列来讲解介绍,对于更前沿的技术点会跟进论文做些文章分享。
最后本系列文章的代码在以下git链接,这部分代码相对零碎主要是配合文章理论来讲的,里面很多片段直接来源于网络整合。后面这可项目会持续维护,把项目代码(应该是c++实现,更体系)、整合进来,根据需要在看看有没必要整合出一个库。
#Bridging the Gap
作者研究了已经建立的 U-Net 肿瘤分割框架在训练来自低资源设置的 MRI 数据以及将其推广到较低分辨率和数据质量方面的能力,并探讨了在资源有限的情况下实现这些框架的可行性。作者的策略是复制一个 BraTS 挑战基准模型,以研究仅包含低分辨率数据的外部数据集的分割预测受到训练数据组成的影响。UNet在新年就又开始大展身手了!广泛接触+局部微调,让UNet无所畏惧!
肿瘤分割模型的一个关键挑战是能够适应各种临床环境,特别是在应用到 poor 质量的神经影像数据时。围绕这种适应性的不确定性源于缺乏代表性的数据集,导致在 Sub-Saharan Africa (SSA) 地区常见于 MRI 数据中的常见伪影的顶级性能模型没有暴露。
作者复制了一个在 2022 年 BraTS 竞赛中获得第二名的框架,以研究数据集组成对模型性能的影响,并通过使用 BraTS-Africa 数据只训练模型(train_SSA,N=60),仅使用 BraTS-成人胶质瘤数据(train_GLI,N=1251),将两者都在一起训练(train_ALL,N=1311),以及使用 BraTS-Africa 数据进一步训练 train_GLI 模型(train_ftSSA)。值得注意的是,仅在较小的低质量数据集上训练(train_SSA)得到的结果不佳,而在仅在较大的高质量数据集上训练(train_GLI)在低质量验证集上难以区分水肿组织。最具有前景的方法(train_ftSSA)是先在高质量神经影像上预训练模型,然后在较小的低质量数据集上进行微调。
这种方法超过了其他方法,在 MICCAI BraTS Africa 全球挑战外部测试阶段排名第二。这些发现强调了改进分割性能的重要性,即更大的样本量和广泛的数据接触。此外,证明了通过使用更广泛的数据进行局部微调,可以改善此类模型的性能。
Introduction
癌症的负担逐年上升,这主要影响低收入和中等收入国家(LMICs),因为这些国家在影像技术以及专家方面的限制,这对于早期诊断和成功治疗至关重要。准确的肿瘤分割为治疗决策提供了重要信息,从而影响了患者的生存率。在颅内肿瘤中,胶质瘤具有高度异质性,尽管在神经肿瘤领域取得了相当大的进展,但预后仍然较差。胶质瘤的形状、大小、边界、强度分布和体积波动都为使用磁共振成像(MRI)进行精确分割带来了挑战。MRI 是脑肿瘤的标准治疗和首选成像模式。
尽管它具有优势,但在低资源环境中仍需解决各种挑战,特别是在撒哈拉以南非洲(SSA)等地区,例如,缺乏高场磁共振扫描仪的可用性以及获取、分析和解释 MRI 数据的合格专家短缺,这导致肿瘤诊断的延迟。此外,由于 SSA 的相对较差的医疗保健系统、社会经济地位以及疾病通常在 SSA 中的晚发性,这进一步恶化了预后并维持了 SSA 的高死亡率。这些因素继续推动颅内肿瘤早期诊断和及时干预的需求。因此,开发能够在各种临床设置上表现良好的精确自动脑肿瘤分割模型在全球范围内具有重要意义。
近年来,脑肿瘤分割领域有了很大的发展,旨在克服依赖领域专业知识、主观性、对噪声和强度非均匀性的敏感性等限制。机器学习技术,特别是深度学习,在这个领域处于前沿,在肿瘤组织、周围水肿和正常组织之间的分割精度方面取得了显著的改进。卷积神经网络(CNNs)和自动编码器的结合是目前最好的方法,已经逐渐减少了依赖领域专业知识和复杂特征提取方法。
具体来说,U-Net模型在自动化脑肿瘤分割任务中已经确立了自己作为黄金标准的地位。为了进一步提高效率、准确性和普遍性,已经提出了几种对U-Net的改进。然而,由于医疗领域的伦理问题和数据隐私规定,用于深度学习模型泛化的神经影像数据集是有限的。
自从 BraTS 挑战赛成立以来,已经从多参数(mp-)MRI 扫描的全球贡献中观察到了显著的脑患者数据增长,其中包含约 4,500 例标注的公开可用脑肿瘤数据集。通过这种合作开发的标准化的自动脑肿瘤分割基准,已经显著推动了该领域的创新架构和训练程序的发展。然而,尽管表现出色,但仍然不确定这些顶级表现的方法是否可以有效地应用于来自撒哈拉以南非洲(SSA)人群的数据,因为这些人群的 MRI 图像质量仍然较差。这主要是由于缺乏代表性的数据集,再加上不同设置之间的图像获取参数的巨大差异,这使得泛化面临着挑战。当前的基准主要是基于高分辨率脑 MRI 在高收入国家的标准资源丰富的临床设置中获得的,并包括图像预处理步骤,这些步骤可能不会让模型暴露于 SSA 常见于常规临床扫描中的伪影。
当前最先进的框架之一是由 Isensee 等人开发的 nn-UNet,它提供了一个强大的肿瘤分割流水线,能够适应各种成像模式和结构 [28]。该框架通过将不同 U-Net 基于架构的预测进行集成,强调了建模决策的重要性,如预处理、数据增强流水线和超参数调优方法。Zeineldin 等人最近表明,对来自撒哈拉以南非洲(SSA)的较小外部测试数据集的期望-最大化集成的 DeepSeg、nn-UNet 和 Deep-SCAN U-Net Pipeline 表现良好;对于整个肿瘤(WT)、肿瘤核心(TC)和增强肿瘤(ET),分别实现了 Dice 分数系数(DSC)0.9737、0.9593 和 0.9022,以及 Hausdorff 距离(95%)(HD95) 分数低于 3.32 的所有子区域。
然而,在具有与训练数据相似特性的外部数据集上的测试并未表现出色,它们的集成方法与 nn-UNet 基础模型的独立运行之间的验证结果差异微小(~0.005,U=6.0,p=0.7)。这可能进一步强调,在模型训练期间增加相关数据表示的简单框架,以及更激进的数据增强和精心选择的后处理方法,可能有助于在各种数据集上实现相似性能的模型开发,并可立即在低资源环境中应用。其中,增加模型训练期间相关数据表示尤为重要。
今年,首次将 BraTS 挑战扩展到包括来自低资源设置的多模态 MRI 训练数据。在这项工作中,作者研究了已经建立的 U-Net 肿瘤分割框架在训练来自低资源设置的 MRI 数据以及将其推广到较低分辨率和数据质量方面的能力,并探讨了在资源有限的情况下实现这些框架的可行性。鉴于挑战的目标是创建一个多功能的基准模型,作者优先考虑与顶级表现方法建立可比指标作为参考,而不是引入复杂的增强。作者的策略是复制一个 BraTS 挑战基准模型,以研究仅包含低分辨率数据的外部数据集的分割预测受到训练数据组成的影响。
Methods
Data Description
本研究中使用的数据集来源于 BraTS 2023 挑战赛的数据。训练、验证和测试数据集包括来自 1565 名术前成人胶质瘤患者的 mp-MRI 扫描,其中 1470 例来自现有的 BraTS 数据(BraTS-Adult Glioma),如前文所述,另外 95 例来自撒哈拉以南非洲(SSA)(BraTS-Africa) 。这些是作为标准护理的一部分在各种机构获得的典型临床扫描,导致成像质量存在显著差异。每个病例包括 T1 加权(T1)、术后钆对比 T1 加权(T1Gd)、T2 加权(T2)和 T2 液体衰减反转恢复(T2-FLAIR)。
图 1 显示了来自每个数据集的患者的样本切片, 强调 BraTS-Adult Glioma 和 BraTS-Africa 之间的数据质量的巨大差异。BraTS-Africa 数据集是通过一个非洲成像中心网络收集的, 得到了非洲 MRI 教育和研究推进联盟(CAMERA)的支持和 Lacuna 基金在健康公平性方面的资金。在每个病例中, 基于图像的肿瘤子区域的真实标注通过迭代过程生成和批准。
Selecting a Framework
自 nn-UNet Pipeline 开发以来,它已经经历了几次修订,主要集中在训练参数上,而不是架构本身。表 1(行 1-3)显示了 2017 年原始 nn-UNet 模型以及 Isensee 等人随后的 BraTS 挑战中提交的修订模型,其中它在 2018 年获得第二名,在 2020 年获得第一名。nn-UNet 有效地解决了在设计更改和选择最佳表现集成时手动考虑相互依赖性的挑战。
然而,参加 2021 年 BraTS 挑战的一些团队表明,nn-UNet Pipeline 中的基本 3D U-Net 组件可以优化以在脑肿瘤分割任务上获得良好性能,而不需要运行涵盖 nn-UNet Pipeline 的整个网络集成。这些团队的比较结果(见表 1,第 4-7 行显示外部验证结果)主要表明,当样本数量保持不变时,整体模型性能存在轻微差异。与使用不同较小样本大小训练的三个 nn-UNet 提交结果的多样性能相比,这可能进一步强调了用于训练分割模型的样本大小的重要性以及其对模型性能的影响。
在选择时, 作者考虑了简单性和有效性, 因为在资源有限的环境中更容易进行复制, 对长期将模型本地化到资源受限的环境更有益。除了样本大小外, Futrega 等人表明, 改变网络架构对整体肿瘤分割性能的影响很小。具体来说, 他们表明, 改变网络层 (例如, 残差连接、多头自注意力)或集成不同的架构(例如, 具有自编码器或基本 U-Net 的残差 U-Net 与视觉 Transformer)与基本 3D U-Net 架构(子区域平均 Dice 分数约为 0.002 )相比仍然具有可比性。
Data Pre-Processing
所有数据都在挑战组织者的初始预处理后提供,以确保在公共数据共享之前删除所有受保护的健康信息。BraTS 标准化的流水线详细描述在 [3, 25] 中使用,预处理步骤包括将 DICOM 文件转换为 NifTI 格式,以删除所有个人患者元数据;去头骨,以毁损神经影像扫描;与 SRI24 解剖模板进行配准;
最后,重采样到统一的 1mm3 等向同性分辨率(见 [11] 中的内容)。然后,作者按照 OptiNet 流水线的描述执行了几个额外的预处理步骤。这些步骤包括:将所有模态的体积堆叠;裁剪冗余背景 Voxel ,以减少计算成本;对非零区域进行归一化;并添加一个前景的 one-hot 编码通道,以区分肿瘤和非肿瘤区域。这最终得到了一个形状为 (5, 240, 240, 155) 的输入张量,其中通道分别表示 4 个模态和 one-hot 编码层。
OptiNet Pipeline and Experiments
OptiNet 主要调整 nn-UNet 框架,涉及与数据预处理相关的建模选择,实现的数据增强,应用的损失函数(区域性,累加二进制交叉熵和 Dice 损失),以及几个后处理步骤。应用的数据增强包括空间变换(大小为 128x128x128 的随机裁剪和随机翻转)和与强度相关的变换(高斯噪声、高斯模糊、亮度变化),并在训练期间实现。
Yousef 等人 [38] 提供了关于基本 U-Net 架构的详细综述:简而言之,它由一个标准的 CNN(卷积神经网络)的收缩(编码器)路径和一个结合了卷积层特征图和三线性插值上采样器的解码器路径组成,用于降采样和上采样。在 OptiNet 版本中,编码器由一个标准的 CNN 组成,与一个结合了卷积层特征图和三线性插值上采样器的解码器相结合。裁剪后的体积大小为 128x128x128,同时保留上下文,通过跳过连接在编码器和解码器之间减少信息损失,帮助解码器恢复图像分辨率和空间结构。
图 2 显示了作者在实验中使用的模型架构,根据提供的公开笔记本 [4] 复制。Futrega 等人 [37] 对 nn-UNet 默认的 UNet 架构进行了轻微的修改(例如,通道大小,上采样技术等)。Futrega 和同事在 2021 年 [35] 和 2022 年 [37] 的论文中详细介绍了他们探索的所有架构和非架构调整。
值得注意的是,他们发现结合其中一些变化与附加的 one-hot 编码通道(约 0.0026,p=0.5;见 [35] 中的表 3)只会稍微增加平均 Dice 分数。
作者进行了四项实验,使用 OptiNet 流水线并改变训练数据集的组成。作者用 1)仅使用 BraTS-Africa 数据进行训练(_train_SSA_,N=60),2)仅使用 BraTS-成人胶质瘤数据进行训练(_train_GLI_,N=1251),3)同时使用两者进行训练(_train_ALL_,N=1311),4)进一步使用 BraTS-Africa 数据训练 train_GLI 模型(_train_figSSA_)。_train_figSSA_ 模型用于估计在非洲局部微调预训练基准模型的可行性。
所有训练和验证都在 Compute Canada 提供的高性能计算集群上使用 Pytorch 1.9 进行的,集群上有一台 NVIDIA T4 Turing(16GB GDDR6 内存)或 NVIDIA V100 Volta(16GB/32GB HBM2 内存)显卡,根据集群上的可用性而定。所有训练都使用内部验证程序,使用训练数据集的划分。由于时间和计算限制,作者将包含更大 BraTS-成人胶质瘤数据集的模型在每轮上分别训练 100 遍。对于仅包含较小 BraTS-Africa 数据集的模型(_train_SSA_ 和 _train_figSSA_),训练设置为最大 150 轮/轮,采用 5 倍交叉验证和当内部验证 Dice 分数在 100 轮内没有改善时采用的早期停止策略。表 2 显示了每轮训练中获得的平均 Dice 分数。每个实验的外部验证预测使用了每个轮内部验证集上的最佳 Dice 分数。
3 Results
以下是针对每个训练变体的性能指标描述,通过挑战在线评估系统计算的验证分数。系统提供的指标(见表 3)是病灶相关的 Dice 分数系数(DSC)和 Hausdorff 距离(95%)(HD95)平均值,跨 15 个用于 BraTS-Africa 2023 验证阶段的患者样本。没有提供真实分割;然而,数据集的较小尺寸允许对所有验证主题生成的分割掩膜进行更详细的审查。本文中呈现的图像是作者模型在选定验证案例上生成的分割。所有分割都由如前述描述的用于标注的非生物标签表示,其中 ED 是绿色,ET 是蓝色,NCR 是红色。
正如预期,仅用 60 个样本训练模型(train_SSA)是不够的,大多数验证分割都是在机会水平上获得的(50%)。为了在类似的数据上提供性能估计以及与其他训练集组成的比较,提交了仅用一次折叠训练 100 个 epoch 的低分辨率 Naive train_GLI 模型进行验证,该模型在包含 219 个胶质瘤患者的 Task 1(BraTS-成人胶质瘤)和 Task 2(BraTS-非洲)验证数据集上表现相对良好(平均 Dice 分数分别为 79.81、82.12、78.48 和 54.04、28.80、42.90,对应 WT、TC 和 ET 的 HD95)。
在 Task 2 验证数据上,肿瘤子区域平均性能指标也很好。由于计划将其用作使用 SSA 数据进一步训练的前训练基础,并检查内部验证损失表明其正在稳定,作者认为该模型足够用于后续使用 BraTS-Africa 数据进行微调。使用 BraTS-成人胶质瘤数据与 BraTS-非洲数据在 5 倍训练过程中进行微调,导致子区域上的平均 Dice 分数相似。
进一步研究子区域性能显示,尽管具有相同的平均 DSC,但 train_GLI 和 train_fISSA 模型在不同子区域上的表现有所不同。_train_GLI_ 模型在区分肿瘤组织和非肿瘤组织方面存在困难,40% 的案例实现了低于机会 DSC。相反,_train_fISSA_ 模型在这个区域表现良好,但在 ET 区域上存在困难,26% 的受试者得分低于机会。ET 子区域通常是最难精确分割的,因为其大小和与周围子区域的重叠。HD95 也提供了更清晰的指示,以评估模型在小型或低质量分割和轮廓重要时的性能。
查看这些分数的差异,作者看到 train_fISSA 模型平均能够比 train_GLI 模型更好地预测整个肿瘤边界(HD95 约 63 个单位较低)。相反,_train_GLI_ 模型在平均上比 train_fISSA 模型更好地区分 ET 子区域(HD95 约 40 个单位较低)。图 3 显示了这些差异明显的情况。
确定了一个异常案例,其中所有模型在准确分割 TC 子区域(平均 DSC 范围 22.33-49.04)方面存在困难,这可能是因为在脑水肿组织中存在类似于 ET 和 TC 的像素强度,这些像素强度存在于脑皮质的后部区域。这个案例对 train_fISSA 的影响大于其他模型:去除这个异常案例后,ET 和 TC 子区域的 DSC 分别增加了 4.13 和 3.43(相应的 HD95 分别降低了 13.04 和 8.04)。此外,这个异常案例还展示了 train_SSA 和 train_ALL 模型在 ET 和 TC 子区域性能的改善,尽管与 train_fISSA 相比较弱。
表 4 显示了平均 DSC 和 HD95 分数的完整差异比较。只有 train_GLI 模型在 TC 子区域受到了影响。此外,一个案例始终在所有模型上实现极其糟糕的分割结果,平均 DSC 跨子区域范围 35.40-37.02,HD95 229.37-229.89,如图 4 所示。
图 5 显示了一个在 ET 和 TC 分割上获得了极好分数(平均 DSC > 89,HD95 < 2.00)但 WT 区域没有很好分割的受试者。传统上,ET 区域的准确分割更具挑战性,因为它通常是一个较小的区域。整个肿瘤本身应该更容易识别。图 5 中,从左到右,每个模型在所示面板的 ET 和 TC 分割上的平均 DSC(HD95)分别为:72.22(9.273)、0.125(285.335)、41.33(190.082)和 89.49(1.414)。从视觉上看,大多数模型主要在区分水肿组织(ED)方面存在困难,而水肿组织非常少。
在回顾所有模型性能时,作者还考虑到了整个肿瘤包含水肿组织,这在低分辨率扫描中更难区分。没有暴露于 SSA 的低质量数据,_train_GLI_ 模型可能很难精确识别伴有广泛水肿的肿瘤的边界。因此,作者将预训练了更大 BraTS-成人胶质瘤数据集并进一步在较小的 SSA 数据集(_train_ftsSA_)上训练的模型提交了外部测试数据集,该数据集包括来自 SSA 的 20 名患者的扫描。在这个未见过的外部测试数据集上,该模型表现良好,在全球范围内排名第二。在撰写本文时,测试阶段获得的最终 DSC 和 HD95 分数尚未公开。
4 Discussion
综上所述,作者的实验结果强调,当前最先进的模型不能直接应用于 SSA 数据,因为可用的训练数据有限,会导致模型过拟合。作者的最终模型在单预训练 100 个 epoch 和 5 倍交叉验证的微调上表现出色。然而,Futrega 等人表明,10 倍交叉验证允许实现更准确的分割性能。作者只验证了 train_SSA 和 train_ftsSA 的 5 倍交叉验证,并在 train_ALL 和 train_GLI 模型上未进行交叉验证。因此,更广泛的交叉验证可能产生更准确的模型比较。
然而,作者的最终模型的高排名性能重申了以前在模拟案例中表明的内容:通过联邦学习,最先进的模型可以通过使用来自不同区域的大量数据进行改进,这使得低资源设置的机构可以在不从外部来源获取数据的情况下,在较小的本地数据集上重新训练。然而,这些结果需要谨慎解释,因为训练和验证数据集的样本量有限。
此外,未来的工作应包括集成来自非洲不同区域的更广泛的数据集,并在使用高质量 MRI 扫描进行训练时,实施与扫描器伪影相关的更强大的数据增强。这些步骤对于开发能够跨各种临床设置看到的图像质量进行泛化的模型至关重要。
#Fus-MAE
一种基于Mask自编码器的自监督学习框架Fus-MAE,它使用交叉注意力在合成孔径雷达和多光谱光学数据之间进行早期和特征 Level 的数据融合。
自监督表示学习框架最近在遥感和传感器社区中引起了兴趣,因为它们有可能减轻制作大型卫星图像数据集的高昂标签成本。在多模态数据融合领域,虽然经常使用的对比学习方法可以帮助弥合不同传感器类型之间的域间隙,但它们依赖于需要专业知识和仔细设计的数据增强技术,尤其是对于多光谱遥感和数据。一种可能的但很少研究的绕过这些限制的方法是使用基于Mask图像建模的预训练策略。
在本文中介绍了一种基于Mask自编码器的自监督学习框架Fus-MAE,它使用交叉注意力在合成孔径雷达和多光谱光学数据之间进行早期和特征 Level 的数据融合,这两种模态具有显著的域差异。
实证结果表明,Fus-MAE可以有效地与针对SAR-光学数据融合的自定义对比学习策略相竞争,并在更大的语料库上训练的其它Mask自编码器框架之上表现出色。
1 Introduction
近年来,多模态学习越来越受到关注,因为有许多模态,如RGB-Depth或文本图像。值得注意的是,最近的研究已经为深度多模态学习相对于单模态的性能优势提供了理论依据。在遥感领域的数据融合领域,有两种模态被广泛研究:合成孔径雷达(SAR)和光学影像。确实,这些模态本质上是互补的:SAR数据具有全天候和穿透云的能力,但存在斑点噪声,使得其解释具有挑战性。另一方面,光学数据虽然受到天气和季节性约束,但具有自然感(例如RGB)和较少的噪声图像,有利于解释。因此,它们的结合对于土地覆盖分类等任务是相关的,并为云去除和SAR去斑等应用打开了大门。
自监督学习(SSL)在各个机器学习领域引起了广泛关注,如自然语言处理(NLP)和计算机视觉。其中一种关键特征是它能够在不需要 Token 数据的情况下学习强大的表示,这在遥感领域尤其有趣,因为数据标注可能代价高昂,通常需要特定领域的专业知识。
近年来,大规模的公共SAR-光学数据集(如SeCo,BigEarthNet-MM,SEN12MS和,最近,SSL4EO-12)的出现,推动了SAR-光学融合的SSL方法的研究。然而,现有的大多数研究倾向于对比学习,虽然有效,但存在一些局限性。这些包括依赖于数据增强,需要仔细设计以适应遥感图像(RSI)的特定性,以及需要负样本,这需要较大的批量大小。
近年来,在Mask图像建模(MIM)方面取得了某些视觉表示学习任务的新状态。尽管MIM避免了对比学习的上述缺陷,但据作者所知,使用MIM进行遥感图像(RSI)数据融合的文献仍然相对较少。在本文中,作者探索了这种替代的预训练方法,用于自监督学习数据融合,贡献可以总结如下:
1.提出了FusMAE,这是一种基于自监督和MAE的框架,能够实现早期级和特征级的数据融合。
2.通过实证方式证明了基于交叉注意力的早期融合策略是最适合预训练SAR-光学数据进行土地覆盖分类任务的策略。
3.展示了FusMAE模型可以在某些最先进的对比学习方法上与针对RSI数据融合的最近方法相竞争。
2 Related Works
在遥感领域中的自监督学习- 根据文献,自监督学习方法可以分为三类:
- 生成方法,其中预任务是在像素级重构被破坏的信号(例如降采样或Mask);
- 预测方法,其中目标是通过预任务(例如预测图像中两个块的相对位置或灰度到RGB着色)学习语义上下文特征;
- 对比学习方法,该方法传统上旨在创建一个嵌入空间,其中相同实例的视图被拉近(正视图),而不相关的视图被拉远(负视图)。
对于SAR-光学数据融合,大部分研究努力都倾向于后者:Chen和Bruzzone研究了SAR和光学图像的早期、中期和晚期融合,通过联合训练两个ResUnets和一个多视对比损失。Montanaro等人使用了SimCLR框架将不同模态的嵌入拉近。Wang等人适应了基于知识蒸馏的DINO框架,该框架不需要负样本,从而省去了需要大型批处理的需求。尽管这些对比方法都相当成功,但它们需要精心设计数据增强 Pipeline 来创建正视图,其质量很难评估。为了绕过这个挑战,作者选择专注于不需要数据增强的生成方法:Mask图像建模。
在遥感领域中的Mask图像建模- He等人[9]最近提出了一种去噪自动编码器(DAE)架构的变体,其中输入图像块以高 Mask 比例随机 Mask ,只留下少量输入块输入到Transformer编码器中。然后,浅层解码器使用获得的潜在和 Mask Token 来重建图像。被称为 Mask 自动编码器(MAE)的框架在ImageNet-1K上设置了新的最先进状态,同时由于处理输入 Token 数量较少和轻量级解码器,训练时间大大缩短。Cong等人将该架构适应光学数据,通过添加多域编码(例如位置+时间或位置+光谱)。Sun等人在2M光学图像数据集上训练了一个基于MAE的模型,并声称在各种RS数据集上实现了最先进性能。Allen等人跟进了一项与SAR图像类似的工作。
然而,尽管在自然领域对Mask图像建模的数据融合方面进行了许多 recent advancements,但SAR-光学方面的文献较少,有些尝试通过在通道轴上堆叠SAR和光学数据来训练MAE,以及一些关于专业 Mask 策略的研究。在本文中提出了一些架构变化来研究早期、中期和晚期融合策略,为未来研究铺平道路。
3 Methodology
作者的工作受到了MultiMAE的启发,MultiMAE是一种基于Mask自动编码器的架构,在自然图像上具有已被证明的记录,其混合流式架构可以接受不同的模态作为输入。在本节中通过在第3.1节中提出需要多任务编码器在第3.2节中提出需要多任务解码器来描述Fus-MAE架构。考虑了两种 Mask 策略,其中详细内容在3.3节中提供。整体架构如图1所示。
Multi-modal encoder
Multi-task decoder
Masking strategies
作者提出研究两种 Mask 策略:独立 Mask 和一致 Mask 。遵循MAE的方法,应用了75%的 Mask 比例,并在各个模态上均匀地采样作者的块。
独立 Mask - 在RS领域中的MiM文献中,跨模态的独立 Mask 被广泛采用,因为它可以捕获模态之间的内相关性和跨相关性。遵循这种策略,作者在各个模态上以均匀的方式随机采样作者的 Mask 块。
一致 Mask - 作者还研究了一种一致 Mask 策略,其中跨模态的 Mask 块相同。作者的假设是,由于SAR和光学数据的域间隙,捕获跨模态相关性比捕获模态内相关性更容易。通过确保作者向各个模态提供表示相同块的 Token ,作者可以减少注意力层捕获跨模态信息时的困难。
4 Experiments and Results
为了研究两种不同的融合策略,作者预训练了两个Fus-MAE实例:仅执行特征级融合的Fus-MAE w/ XAD,以及同时执行早期和特征级融合的Fus-MAE w/ XAE&D。在BigEarthNet训练分割的354,196张图像上训练Fus-MAE,使用AdamW优化器,批处理大小为200,共100个Epoch。在前10个Epoch中,学习率逐步升温到,然后使用余弦计划进行衰减。
作者在2台NVIDIA RTX 3090Ti上训练Fus-MAE,大约需要60小时。将ImageNet初始化作为 Baseline ,并使用两个专门为SAR-光学数据融合设计的基于Transformer的预训练模型来完成作者的基准测试:分别代表最近对比学习研究的DINO-MM和SATVIT,以及分别代表Mask图像建模的近期研究。
多标签分类 - 为了评估预训练策略的有效性,在预训练的编码器顶部附加一个线性分类头。在10个Epoch中微调模型,使用PyTorch的MultiLabelSoftMargin Loss和AdamW优化器,并报告测试分割上的平均平均精度(mAP)得分。
表1总结了这些微调实验的结果,其中分类器在单模态数据(S1或S2)或多模态数据(S1+S2)上进行训练。值得注意的是,与其它架构相比,Fus-MAE展示了优越的性能,对于S2和S1+S2,mAP提高了大约2个百分点,而Fus-MAE w/XAE&D略有优势。
然而,值得注意的是,在所有架构中,使用仅SAR数据(S1)的mAP显著低于使用S2或S1+S2时的情况,这表明所有模型在S1+S2场景中主要依赖光学数据进行预测。
此外,为了评估在标签和资源稀缺条件下学习的表示的质量,在训练标签的1%上进行线性评估。在这种情况下,线性块投影和编码器权重被冻结,只允许学习线性分类器的权重。在AdamW优化器的支持下训练这个分类器20个Epoch,批量大小为128。结果也在表1中报告,与其它SSL架构相比,性能提高更大,突显了Fus-MAE学习的表示的质量。
迁移学习 - 为了研究泛化能力,作者在另一个下游任务上进行线性评估:在SEN12MS数据集上进行单模态土地覆盖分类。由于这个数据集的不平衡性质,使用加权平均方法计算精确度、召回率和F1-score。在AdamW优化器的支持下训练这个分类器10个Epoch,批量大小为256,使用Timm的LabelSmoothingCrossEntropy损失。与之前实验类似的实验设置下的基准测试结果报告在表2中。Fus-MAE在所有跟踪指标上均优于其他技术,尽管优势较小。
5 Conclusion
在本文中提出了Fus-MAE,这是一个新颖的用于SAR-光学数据融合的自监督学习框架。基于MAE架构,它使用在不同阶段两个数据流之间的交叉注意力来执行早期和特征级数据融合。
Fus-MAE在各种下游任务上超过了最近对比学习和基于MIM的工作,证明了使用交叉注意力描述具有较大域间隙的模态之间跨模态交互的有效性。可以进一步研究将作者的交叉注意力层扩展到超过2个模态,并使Fus-MAE在各个模态上的预测依赖更加均衡。
#Weight Selection
作者提出了一种称为权重选择 (Weight Selection) 的方法,这是一种从预训练好的大模型中选择权重来初始化较小模型的方法。Weight Selection 把知识从预训练好的权重迁移到更小的模型中。权重选择:用大神经网络的权重初始化小神经网络
权重初始化是神经网络训练中的一个古老而又永恒的话题。对于从头训练的神经网络而言,有很多权重初始化的方法。近年来大模型的出现,为这一经典的科研课题带来的新的机会。
在本文中,作者提出了一种称为权重选择 (Weight Selection) 的方法,这是一种从预训练好的大模型中选择权重来初始化较小模型的方法。Weight Selection 把知识从预训练好的权重迁移到更小的模型中。
本文的实验结果表明,Weight Selection 可以显着提高小模型的性能并减少它们的训练时间。而且,值得一提的是,它可以与知识蒸馏方法一起使用。Weight Selection 提供了一种在资源受限的环境中借助预训练模型强大力量的新方法,本文希望它可以使大模型时代训练小模型的有用工具。
1 用大神经网络的权重初始化小神经网络?
论文名称:Initializing Models with Larger Ones
论文地址:https//arxiv.org/pdf/2311.18823.pdf
1 Weight Selection 论文解读:
1.1 重新思考神经网络初始化
神经网络权重的初始化 (Weight Initialization) 对其后续的优化过程至关重要。合适的初始化权重可以有助于模型的收敛以及防止梯度消失的问题。当前有两种比较著名的初始化技术:Xavier 初始化[1]和 Kaiming 初始化[2],在神经网络的训练中发挥了重要作用。它们至今为止仍然是 PyTorch 等现代深度学习库的默认方法。
这些方法是为从随机初始化进行训练的神经网络研发的,在当时是很常见的做法。然而,在大模型时代,由于社区的集体努力,很容易获得各种预训练模型。一般而言,这些预训练模型背后的数据集包含 ImageNet-21K[3]和 LAION-5B[4] 等等。因此,从这些预训练模型进行微调,而不是从头开始训练模型,通常被认为是当今时代的首要选择,如下图1所示。
然而,这些预训练的大模型在资源需求上可能令人望而却步,严重阻碍了它们在资源受限设备 (比如移动设备) 上的使用。对于很多预训练模型族 (model zoo),即使是最小的模型在某些情况下也可以被认为非常大。比如MAE[5]和CLIP[6]模型,两者的最小预训练 Transformer 模型是 ViT-Base[7]模型,包含 80M 参数量。对于边缘设备的应用来说,这已经太大了,最小的 LLaMa[8]模型甚至大 100 倍,包含 7B 参数量。由于可以直接使用的预训练模型很少,开发人员必须在目标数据集上面从头开始训练模型,以适应他们的需求。这种方法错过了使用大型预训练模型的机会。这有点可惜,因为大模型的知识是从大数据集里面好不容易学到的,不加以合理利用就有点可惜了。
图1:大模型时代,预训练权重为小模型的学习带来新的生机
1.2 本文贡献
本文作者通过引入一种权重初始化 (Weight Selection) 技术来解决上面的问题,该方法使用预训练大模型来训练小模型。具体而言,本文提出一种 Weight Selection 技术,选择来自预训练大模型的权重子集来初始化较小的模型,也就是允许大模型学习到的知识通过其权重转移到小一点的模型里面。
而且,由于现代神经网络的模块化设计,权重选择只涉及3个简单的步骤:Layer Selection, Component Mapping, 和 Element Selection。该方法可以应用于与大模型相同的模型族中的任何小模型。使用权重选择来初始化小模型很简单,并且与从头开始训练相比没有任何额外的计算成本。即使对于大模型训练也很有用,比如使用来自 LLAMA-30B 的训练权重初始化 LLAMA-7B。
1.3 Weight Selection 方法介绍
给定一个预训练模型,Weight Selection 的目标是在同一个模型族 (model zoo) 中,为小一点的模型找到更高效的初始化,如下图2所示。将预训练的大模型称为 Teacher,小一点的模型称为 Student。
由于现代神经网络架构通常遵循模块化方法,即:堆叠很多相似的 layer 来构建最终的模型 (主要目的是为了方便模型的可扩展性)。因此,在选择 Teacher 中的权重来初始化 Student 模型时就可以借助这个特点节约很多力气。
在本文中,作者通过3步来实现权重选择:Layer Selection, Component Mapping, 和 Element Selection。
- Layer Selection 就是选择 Teacher 中的哪一层来初始化 Student 模型。
对于学生模型的每一层,都要选择教师模型的一层来进行初始化。学生模型分为 Isotropic 和 Hierarchical 两种架构:
- Isotropic 就是直筒型架构,从头到尾都是相同的分辨率和特征维度。Isotropic 架构的典型代表有 ViT 和 MLP-Mixer。
- Hierarchical 就是金字塔式架构,从开始到结束分为多个 Stage,各个 Stage 之间特征分辨率逐渐降低,而特征的维度逐渐升高。Isotropic 架构的典型代表有 Swin Transformer 和 ConvNeXt。
图2:Weight Selection 的步骤
- Component Mapping 就是选择 Teacher 中一层的哪个组件来初始化 Student 模型对应层的什么组件。
上一步获得了从教师到学生的 Layer 的映射。Component Mapping 就简化了为用一个 Teacher layer 初始化一个 Student layer。由于现代神经网络设计采用的模块化方法,相同 model zoo 里面不同大小的模型的 Component 一般都是一致的,只是特征的通道数不同,或者说宽度不同。因此,Component Mapping 就是一对一映射的关系。
- Element Selection 就是选择 Teacher 中某个组件的哪部分权重来初始化 Student 中对应组件的权重。
均匀选择 (Uniform Selection)
本着简单的原则考虑,默认方法是均匀选择 (Uniform Selection),即从图2中教师模型的某个权重中拿出间隔均匀的权重元素来初始化学生模型。接下来介绍下 Uniform Selection 的具体做法。
图3:Uniform Selection 的伪代码
1.4 实验配置
数据集:9个图像分类数据集:ImageNet1K, CIFAR-10, CIFAR-100, Flowers, Pets, STL-10, Food-101, DTD, SVHN 和 EuroSAT。
模型:如下图4所示。
Student 模型:ViT-T/16 和 ConvNeXt-F。Teacher 模型:ImageNet-21K 预训练的 ViT-S/16 和 ConvNeXt-T。
图4:模型配置
训练策略:如下图5, 6, 7所示,本文作者遵循 ConvNeXt 的训练策略,并对不同数据集的 Batch Size、学习率和 stochastic depth rate 进行调整。为了确保公平的比较,作者将这套训练策略也应用于 Baseline 模型。
图5:训练策略
图6:ConvNeXt-F 的超参数设置
图7:ViT-T 的超参数设置
1.5 实验结果
实验结果如下图8所示。在所有 9 个图像分类数据集中,本文的 Weight Selection 方法都可以提高精度,尤其是对于小数据集而言更是如此。值得注意的是,Weight Selection 方法解决了在小数据集上训练 ViT 的难题,有助于 ViT 精度的显著提高。ImageNet-1K 的训练曲线如图9所示。两种模型都受益于早期的权重选择,在整个训练过程中保持这一优势。
图8:9个图像分类数据集实验结果
图9:ViT-T 和 ConvNeXt-F 在 ImageNet-1K 上的训练曲线
1.6 与其他方法的比较
作者在 CIFAR100 数据集上面对比了其他几种初始化方法的结果,如图10所示。
而且,还可以发现一致性是 Weight Selection 达到最佳性能的关键。对于这两种模型架构 ViT-T 和 ConvNeXt-F,均匀选择,连续选择,以及一致性随机选择的最终性能相似。在去除一致性时,性能急剧下降。
图10:与其他几种初始化方法的比较
1.7 与知识蒸馏的兼容性
权重选择 (Weight Selection) 通过参数从预训练模型转移知识。另一种流行的知识转移方法是知识蒸馏 (Knowledge Distillation),它利用了预训练模型的输出。在这里,作者也探讨了这2种技术的兼容性。
实验设置:作者尝试了2种知识蒸馏方案:logit-based distillation 和 feature-based distillation。
图11:与知识蒸馏的兼容性的实验结果
1.8 其他分析结果
权重选择可以减少训练时间。如下图12所示为作者使用权重选择方法训练不同 Epoch 数量,得到的曲线与随机初始化训练 300 Epoch 的结果对比。可以看到,权重选择与随机初始化训练相比,CIFAR-100 的相同性能只需 1/3 个 Epoch 即可获得。
图12:与随机初始化的比较
迁移学习结果对比。如下图13所示为作者希望看看先在 ImageNet-1K 上预训练多少 Epoch,再在 CIFAR-100 上做 fine-tune 的结果与权重选择的结果相当。可以发现,在 ImageNet-1K 上进行 60 个 epoch 的预训练之后微调,可以实现与权重选择相当的性能。在这种情况下,与预训练达到相同的性能相比,权重选择快 6.12 倍,且无需访问用于预训练的数据集。
图13:预训练与微调结果比较
层的选择方案。如下图14所示为作者以 ViT-A 和 ConvNeXt-F 为学生模型,ImageNet-21K 上预训练的 ViT-S 和 ConvNeXt-T 作为权重选择的教师模型。可以看到,使用 \text{First-N}\text{First-N} 选择的结果最佳。用 \text{First-N}\text{First-N} 选择做初始化的层自然是连续的层,而且更接近输入处理,因此它们为较小的模型提供了更有效的初始化。
图14:层选择方案对比
教师模型尺寸。如下图15所示为作者对比了不同尺寸教师模型的结果。可以看到,教师模型大小更接近时,权重选择会得到更好的结果。教师模型很大意味着将丢弃更高百分比的参数,这会在权重选择过程中导致更多的信息丢失。图15的结果中有趣的是,即使是使用 ViT-L 这样的模型初始化,从 301M 参数中选取 5M 参数,也很有效,精度提升了 4.5%。
图15:不同尺寸教师模型结果对比
图16:与剪枝方法的对比
与模拟初始化 (Mimetic initialization[12]方案对比。如下图17所示为作者对比了权重选择方法与模拟初始化 (Mimetic initialization[12] 方法。权重选择方法大大优于模拟初始化。
图17:与模拟初始化方法对比
图18:注意力层可视化
#DeepDR
本项研究创新性提出了基于Weibull混合分布模型的疾病进展分析深度学习框架,创造性地将糖尿病视网膜病变的进展和发生时间视为筛查区间内的随机变量,通过生存分析与时序分布概率建模,成功实现了对糖尿病视网膜病变进展的风险预警和时间预测。
1月4日,上海交通大学电子信息与电气工程学院计算机科学与工程系/教育部人工智能重点实验室盛斌教授团队,上海交通大学医学院附属第六人民医院内分泌代谢科、上海市糖尿病重点实验室贾伟平教授和李华婷教授团队,清华大学副教务长、医学院主任黄天荫教授团队,在国际权威刊物《Nature Medicine》(《自然医学》)发表题为“A deep learning system for predicting time to progression of diabetic retinopathy”(用于预测糖尿病视网膜病变进展时间的深度学习系统)的科研成果。该成果是医工交叉团队继2021年成功完成糖尿病视网膜病变辅助智能诊断系统“DeepDR”的研发之后,进一步构建的基于时序影像序列深度学习的糖尿病视网膜并发症预警系统“DeepDR Plus"。该系统可基于眼底图像精准预测糖尿病视网膜病变进展,成果为推动全球糖尿病并发症的智能防控贡献了中国力量,有望为全球糖尿病视网膜病变的筛防新策略的制定提供指引。
研究介绍
糖尿病视网膜病变(diabetic retinopathy, DR)是糖尿病最常见的微血管并发症,也是全球可预防失明的主要原因。该病初期症状隐匿,病情严重时可能导致永久视力损伤甚至失明。由于不同患者病情进展存在较大差异,每位糖尿病病人患DR的风险和时间难以准确预测。目前,以深度学习为代表的人工智能技术已被用于DR筛查,然而基于眼底图像来预测DR发生风险仍是全球关注的重难点问题。在糖尿病等相关慢病诊疗和管理的临床实践流程中,糖尿病患者往往只会按照相对固定的时间间隔进行筛查或随访,并发症的确切发生或进展时间无法知晓,这也导致传统深度学习模型无法实现疾病进展时序轨迹的精准建模,进而无法预测个体的发病和进展时间点。针对这一困扰全球糖尿病管理的关键技术瓶颈与临床需求,本项研究首次基于大规模医学影像纵向队列,涵盖多国多种族的超20万名糖尿病患者的眼底图像和临床数据,创新性提出了基于Weibull混合分布模型的疾病进展分析深度学习框架,创造性地将糖尿病视网膜病变的进展和发生时间视为筛查区间内的随机变量,通过生存分析与时序分布概率建模,成功实现了对糖尿病视网膜病变进展的风险预警和时间预测。研究团队通过将该系统应用于中国和印度的真实临床流程,证实该系统可在大幅降低筛查频率和公共卫生成本的情况下仍保持极低的漏诊率,从而为将来的糖尿病并发症防控实践提供了个性化筛查和管理决策的依据。
DeepDR Plus系统概览和研究设计
早期筛查和干预对于DR的预防和管理至关重要。国内和国际组织大多建议无或轻度DR的糖尿病患者每年进行常规眼底摄片检查,以便及时发现视网膜病变并进行干预。然而,由于经济和医疗资源等因素的限制,尤其是在中、低收入国家,糖尿病患者常规眼底摄片检查的实施和普及困难重重。本研究首次实现了个体化糖尿病视网膜病变风险和时间预测,DeepDR Plus系统仅根据基线眼底图像,准确预测未来5年DR进展的个体化风险和时间,优于传统临床参数模型。此外,DeepDR Plus系统可以准确识别高、低风险人群,提供了人工智能驱动的个性化的推荐随访间隔(低风险患者给予相对更长时间的随访间隔建议而几乎不导致威胁视力DR的漏诊)和管理策略(高风险患者给予相对更严格的综合干预建议)。
人工智能驱动的个性化筛查间隔纳入糖尿病视网膜病变筛查系统,特别在发展中国家,可以极大地提高眼底摄片筛查的效率、公平性和可及性。该研究为糖尿病视网膜病变筛查、预防和诊疗指南提供了新的证据,有望对未来糖尿病视网膜病变的临床诊疗流程和医疗费用等产生重要影响,在世界地图上为糖尿病智能防控贡献了中国技术和亚洲力量,为一带一路及全球中低收入国家和地区的糖尿病管理模式的提质增效与改革创新开辟了新道路。
#DATM~
作者分析了生成数据的难易以及其对压缩效果的影响,发现应该根据压缩比率控制生成数据的难易。通过应用这一策略,作者提出了第一个可以在低压缩率保持有效的数据集蒸馏方法,并首次实现了无损数据集蒸馏。
论文题目:
Towards Lossless Dataset Distillation via Difficulty-Aligned Trajectory Matching
论文地址:https://arxiv.org/abs/2310.05773
代码地址: https://gzyaftermath.github.io/DATM/
一、背景
数据集蒸馏旨在以生成的方式将一个大的数据集压缩成一个小的数据集。得益于生成数据的高信息密度,在压缩率较高时,数据集蒸馏的效果远超数据集剪枝等其他数据压缩方法。
然而,随着压缩率的逐渐降低,现有数据集蒸馏方法的效果逐渐变差,甚至弱于从原数据集中随机选取等量数据。这导致现有数据集蒸馏方法局限于高压缩率场景,无法实现对原数据集的无损压缩。
在本文中,作者分析了生成数据的难易以及其对压缩效果的影响,发现应该根据压缩比率控制生成数据的难易。通过应用这一策略,作者提出了第一个可以在低压缩率保持有效的数据集蒸馏方法,并首次实现了无损数据集蒸馏。
二、动机
数据蒸馏可以看作是一个从目标数据集提取特征并将之添加到生成数据上的过程。
目前,主流的数据集蒸馏方法大多通过匹配模型在目标数据集和生成数据集上的某种指标来完成特征的提取与压缩。其中,基于匹配模型训练轨迹[1](Matching Training Trajectory)的方法在多个基准下达到了最优。由于该方法通过匹配模型在生成数据集和目标数据集上的训练轨迹(匹配模型参数)来进行蒸馏,因此其效果受模型在目标数据集上的训练轨迹影响。
根据文献[2]可知,对于分类任务而言,模型倾向于在训练前期学习目标物体的简单特征而在后期学习难特征。因此我们推测匹配模型的前期轨迹就会倾向于在生成数据上添加更多的简单特征而匹配后期轨迹则会倾向于添加难特征。意识到这一点,我们开始探索匹配不同阶段路径对于数据集蒸馏的影响。
图一、匹配不同阶段的轨迹的蒸馏效果
实验结果如图一所示,当IPC较低(即压缩率较高)时,匹配前期轨迹(即添加简单特征)是最有效的。而随着压缩率的降低,匹配后期轨迹则变成了最优选择,此时匹配前期路径甚至变得有害。基于这些实验现象,我们提出根据压缩比率控制生成特征的难易,并基于此提出了我们的方法。
三、方法
四、实验结果
3.1 效果比较
我们在CIFAR10,CIFAR100和TinyImageNet数据集下和多个有代表性的数据集蒸馏方法进行了对比,并进一步探索了数据集蒸馏在高IPC情境下的表现。主要的实验结果如下,更多的结果请参考我们的论文。
和多个蒸馏方法在不同数据集和不同IPC(image per class)设定下的比较
在高IPC情境下数据集蒸馏和数据集剪枝的表现比较
3.2 分析
简单特征和难特征有什么特点?为什么压缩率较低时添加简单特征有害?我们对这些问题进行了探索。如下图所示,匹配前期轨迹(添加简单特征)会更大幅度的改变图片内容,将目标物体融入背景,使图片有更多关于目标物体的基本信息。这使得添加简单特征在压缩率较高时有效,因为此时蒸馏数据集太小以至于模型无法很好的学习到各个类别的基本特征。
匹配处于不同训练阶段的轨迹所得的蒸馏样本
此外,我们可以观察到添加简单特征会让图片损失关于目标物体的细节信息,而添加难特征(匹配后期轨迹)则会让目标物体具有更丰富的纹理细节。因此在压缩率较低时添加难特征的表现更好,因为此时蒸馏数据集本身就含有足够多的数据让模型学会基本的类别特征,添加更多的细节有利于模型识别离群样本。下图是蒸馏图片的可视化,更多分析请见论文。
不同IPC设定下蒸馏所得样本
五、总结
在这项工作中,作者发现应该根据压缩比率控制生成样本的难度。通过应用这一思想,作者提出了第一个可以在高/低压缩率皆能保持有效的数据集蒸馏算法,并首次实现了无损数据集蒸馏,将CIFAR-10,CIFAR-100压缩到了原数据集的1/5大小而不对训练ConvNet造成性能损失。
#MOSS-RLHF
RLHF中Reward model的trick, 作者们在报告中提出了一系列方法来解决如何增加RM的泛化能力的问题,从数据和算法角度,分别提出了两个问题核心问题和对应的解决方法,旨在提高奖励模型在处理错误偏好数据和泛化到新分布数据时的性能。
引入
在大家搞RLHF中经常遇到的一个核心的问题是,RM的水平不够好没法训练得到想要的效果,其背后两大类基本的原因是:1.数据质量低。2.模型泛化能力差。复旦MOSS这篇技术报告,从这两个问题入手,提出了一系列方法优化和提升。也是Secrets of RLHF 系列的第二篇。干货十足,细节丰富,推荐阅读。
https://arxiv.org/abs/2401.06080
https://github.com/OpenLMLab/MOSS-RLHF
核心问题
展开来讲的话,关于1.数据质量低 2.模型泛化能力差这两个问题具体指的是:
一、数据质量低:数据集中的错误和模糊的偏好对(pairs),可能导致奖励模型(RM)无法准确捕捉人类的偏好。你通过数据透传给你的模型,一会儿向左,一会儿向右,模型也要学懵。
二、泛化能力差:奖励模型在特定分布上训练后,很难泛化到该分布之外的例子,且不适合迭代式的RLHF训练(提升RLHF的重要路径之一)。你的模型训练得到了一个二极管,对于自己相信的东西表现的非常极端,对于没见过的东西就傻眼了。
针对这两类问题,作者提出了两个视角的方法,分别从数据角度和算法角度出发。
一、数据角度:使用多个奖励模型的输出,增加数据度量的信息源,用这种方法来量化数据中偏好的强度信息,并通过这种方法来识别和纠正错误或模糊的偏好对。对于不同质量水平,模糊度水平的数据,采取了不一样的措施,有翻转,软标签,适应性margin等具体方法,后面具体展开讲解。
二、算法角度:借助对比学习和元学习的方法。增加对比学习的loss,对比学习通过增强模型区分能力,来增强RM的对好坏的区分水平。元学习则使奖励模型能够维持区分分布外样本的细微差异,这种方法可以用于迭代式的RLHF优化。
数据视角
可以看出数据的区分性比较强,并且随着的上升,和GPT4标注结果的一致性也在上升。
通过如上的方法就可以把数据大概分开,我们划分为3类进行分析。
1.低强度的偏好数据的负面影响:研究发现,数据集中偏好强度最低的20%的数据对模型在验证集上的性能有负面影响。这些数据的偏好强度平均值小于0,表明这些数据可能包含错误的偏好标签。
2.中等强度偏好数据的中立影响:偏好强度在20%到40%之间的数据,在训练后,模型在验证集上的预测准确率大约为0.5。这些数据的偏好强度平均值接近0,表明这些数据的偏好差异不大,模型在这些数据上的学习效果一般。
3.高强度的偏好数据的积极影响:剩余的数据(偏好强度最高的60%)显著提高了模型的性能。然而,仅使用偏好强度最高的10%的数据训练模型时,并没有达到最佳性能。这可能是因为这些数据过于强烈,导致模型可能过度拟合这些数据。
归纳出偏好强度信息后,我们可以根据偏好强度的测量结果,可以对数据集进行分类,并对不同类别的数据采取不同的处理策略。
对于低强度的偏好数据,隐含标签错误的可能性,通过翻转偏好对的标签可以有效地提高模型性能。对于中强度的,比较模糊的偏好数据,应用软标签和适应性边距可以避免模型过度拟合。对于高强度的偏好数据,使用软标签和适应性边距的组合特别有效。
adaptive margin
一种让同类聚集,异类区分度增大的经典方法,来自于人脸识别的经典方法。
作者给了这几种方法的详细实验过程:包含了reward,loss,ppl,输出len等角度的度量。
整体看起来,软标签适用在中上强度的偏好数据,margin方法在所有强度数据都适用。
算法视角
在论文的 "Preference Generalization and Iterated RLHF" (偏好泛化和迭代RLHF) 部分,作者们提出了两种主要的方法来提高奖励模型 (Reward Model, RM) 的泛化能力,使其能够在分布变化的情况下仍然能够有效地区分不同的响应。具体做法如下:
一、对比学习 (Contrastive Learning):
二、MetaRM(Meta Reward Model)
提出了一种名为MetaRM的方法,通过元学习来对齐原始偏好对与分布变化。MetaRM的关键思想是在训练阶段最小化原始偏好对的损失,同时最大化对从新分布中采样的响应的区分能力。
训练过程: MetaRM的训练过程包括四个步骤: 计算从新分布中采样的响应的差异损失,计算损失相对于RM参数的梯度并更新参数,计算原始偏好对的损失,以及计算损失相对于更新后的参数的梯度并优化原始参数。
具体,MetaRM 的算法包括以下步骤:
通过这些方法,奖励模型能够更好地捕捉数据中细微的偏好差异,从而在面对新分布的数据时保持其区分能力。这使得奖励模型能够在迭代的RLHF过程中更稳定地优化语言模型,即使在模型输出分布发生变化时也能保持其指导优化的能力。
主要实验结果如图所示:MetaRM 在分布内和分布外任务评估中都显示出了优越的性能。在分布内任务中,MetaRM 在多个回合的 PPO 训练后,其性能显著优于基线模型。
另外在OOD上的表现做了单独的分析,在分布外任务中,MetaRM 继续优于基线模型,表明其方法能够有效地在新领域实现对齐,而无需对一组query进行成本高昂的重新标注。
总结
总结来说,作者们在报告中提出了一系列方法来解决开头提到的核心问题,如何增加RM的泛化能力,从数据和算法角度,分别提出了两个问题核心问题和对应的解决方法,旨在提高奖励模型在处理错误偏好数据和泛化到新分布数据时的性能。
#后向传递の计算量
从前向传递、后向传递和优化器参数更新的浮点数计算次数入手,详解向传递的耗时为啥几乎是前向传递的两倍。后向传递的计算量大约是前向传递的两倍
1.
训练神经网络的一次迭代分为三步:(1)前向传递计算损失函数;(2)后向传递计算梯度;(3)优化器更新模型参数。在实验中,我们观察到一个现象:后向传递的耗时几乎是前向传递的两倍,相比之下,优化器更新的耗时几乎可以忽略。要解释这个现象,我们要从前向传递、后向传递和优化器参数更新的浮点数计算次数入手。
上图表示一次训练迭代中各个环节(前向传递、后向传递、通信环节、优化器更新)的耗时占比,来自于《PyTorch Distributed: Experiences on Accelerating Data Parallel Training》。上图中,纵轴表示耗时占比,FWD表示一次训练迭代中前向传递的耗时占比,BWD则表示一次训练迭代中后向传递的耗时占比,OPT表示一次训练迭代中优化器更新模型参数的耗时占比。从上图中可以看到,一次训练迭代中,后向传递的耗时几乎是前向传递的两倍,相比之下,优化器更新的耗时占比很小,几乎可以忽略。
上图表示GPipe流水线并行的调度策略,来自于《Efficient large-scale language model training on gpu clusters using megatron-lm》。上图中,横轴表示耗时,一个蓝色小块表示一个微批次的前向传递,一个绿色小块表示一个微批次的后向传递,黑色竖线表示一次流水线刷新,也就是优化器更新模型参数。从上图中可以看到,一个绿色小块的宽度大约是蓝色小块的二倍,一次训练迭代中,后向传递的耗时几乎是前向传递的两倍,相比之下,优化器更新的耗时占比很小,几乎可以忽略。
2.反向传播算法是怎么工作的
反向传播算法已经是训练神经网络模型不可缺少的一部分。训练神经网络模型时,用梯度下降算法来学习和更新模型参数(包含权重weights和偏置bias),问题是如何计算损失函数关于模型参数的梯度呢?这就要用到反向传播(backpropagation)算法。
2.1 前向传递:计算神经网络的输出
在讨论反向传播算法之前,我们先以多层前馈神经网络为例,用基于矩阵的方法来计算神经网络的输出。在此过程中,先定义一些数学符号。
2.2 反向传播的四个基本等式
2.2.2 误差与下一层误差的关联
带入上上式中,得到:
2.2.3 偏置的梯度
2.2.4 权重的梯度
2.2.5 四个基本等式
综上,梯度反向传播的四个基本等式是:
有了这四个基本等式,我们就基本理解了梯度的计算过程。
3. 后向传递与前向传递的FLOPs比率
3.1 定义
FLOPS:全大写,floating point operations per second,每秒钟浮点数计算次数,理解为运算速度,是衡量硬件性能的指标。例如:A100的float32运算速度是9.7TFLOPS,V100的float32运算速度是7.8TFLOPS。
FLOPs:s为小写,floating point operations,表示浮点数运算次数,理解为计算量,衡量模型或算法的复杂度。
backward-forward FLOP ratio:后向传递与前向传递的FLOPs比率,表示一次后向传递的浮点数计算次数与一次前向传递的浮点数计算次数的比率。衡量了一次后向传递要比一次前向传递多进行的浮点数运算。
3.2 前向传递与后向传递的浮点数操作次数的理论分析
为了理解后向传递的浮点数运算为什么要比前向传递多?我们就要从反向传播算法的四个基本等式入手。
我们从一个有2层隐藏层的神经网络入手:
我们假设线性层采用ReLU激活函数,采用随机梯度下降优化器。下表中h1和h2分别表示第1层和第2层隐藏层的神经元个数。有2层隐藏层的神经网络一次训练迭代的计算过程如下表所示:
从上表中,我们可以观察到,对于多层前馈神经网络模型,有以下结论:
1. 相比于线性层,激活函数ReLU和损失函数的浮点数运算量可以忽略。
2. 对于第一层,后向传递-前向传递的FLOPs比率是1:1。
3. 对于其他层,后向传递-前向传递的FLOPs比率是2:1。
4. 采用随机梯度下降作为优化器,权重更新的FLOPs是模型参数规模的2倍。
3.2.1 第一层与其他层的区别
对于多层前馈神经网络模型,采用随机梯度下降作为优化器。
第一层的后向传递-前向传递的FLOPs比率是1:1,其他层的后向传递-前向传递的FLOPs比率是2:1。模型参数更新的FLOPs是模型参数规模的2倍。
3.2.2 batch_size的影响
前向传递和后向传递的计算量FLOPs与batch_size成正比,即随着batch_size增大而线性增长。
优化器更新模型参数的FLOPs与batch_size无关,只与模型参数规模和优化器类型有关。
随着batch_size增大,前向传递和后向传递的FLOPs线性增长,而权重更新的FLOPs保持不变,这使得权重更新的FLOPs变得逐渐可以忽略不计了。
3.2.3 网络深度的影响
神经网络层数对后向传递-前向传递FLOPs比率有着间接的影响。由于第一层的后向-前向FLOPs比率是1:1,而其他层后向-前向FLOPs比率是2:1。层数的影响实际上是第一层与其他层的比较。
定义一个CNN神经网络,中间层的数量由0到100,可以看到后向-前向FLOPs比率由1.5提高到了接近2的水平。当层数逐渐变深的时候,第一层对模型整体的FLOPs影响就变小了,模型整体的后向-前向FLOPs比率就很接近2。
4. 总结
随着batch_size的增大,前向传递和后向传递的FLOPs线性增长,而权重更新的FLOPs保持不变,参数更新的FLOPs变得可以忽略不计了。这体现为:当batch_size足够大时,在训练神经网络的一次迭代中,前向传递和后向传递是主要的耗时环节,而参数更新的耗时变得几乎可以忽略不计。
由于第一层的后向-前向FLOPs比率是1:1,而其他层后向-前向FLOPs比率是2:1。随着网络层数的加深,第一层对整体FLOPs的影响变得可以忽略不计了。这体现为:当网络层数足够深时,后向传递的耗时几乎是前向传递耗时的2倍。
总的来说,对于用大batch_size的深层神经网络来说,后向传递-前向传递的FLOPs比率接近于2:1,换句话说,后向传递的计算量大约是前向传递的两倍。