开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

分布式事务实现方案:一文详解RocketMQ事务消息 实现原

发表于 2024-04-26

常见的分布式事务实现方案有以下几种:两阶段提交(2PC)、两阶段提交(2PC)、补偿事务(Saga)、MQ事务消息等。今天就讲一下 RocketMQ 的事务消息,是一种非常特殊的分布式事务实现方案,基于半消息(Half Message)机制实现的。

看完这篇想一下,RocketMQ事务消息到底能不能保证分布式系统中数据的强一致性?

实现原理

RocketMQ事务消息执行流程如下:

  1. 生产者将消息发送至RocketMQ服务端。
  2. RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为”暂不能投递”,这种状态下的消息即为半事务消息(Half Message)。
  3. 生产者开始执行本地事务逻辑。
  4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
    • 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
    • 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
  5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
  6. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  7. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。

image.png

代码实现

RocketMQ事务消息示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码//演示demo,模拟订单表查询服务,用来确认订单事务是否提交成功。
private static boolean checkOrderById(String orderId) {
return true;
}

//演示demo,模拟本地事务的执行结果。
private static boolean doLocalTransaction() {
return true;
}

public static void main(String[] args) throws ClientException {
ClientServiceProvider provider = new ClientServiceProvider();
MessageBuilder messageBuilder = new MessageBuilderImpl();
//构造事务生产者:事务消息需要生产者构建一个事务检查器,用于检查确认异常半事务的中间状态。
Producer producer = provider.newProducerBuilder()
.setTransactionChecker(messageView -> {
/**
* 事务检查器一般是根据业务的ID去检查本地事务是否正确提交还是回滚,此处以订单ID属性为例。
* 在订单表找到了这个订单,说明本地事务插入订单的操作已经正确提交;如果订单表没有订单,说明本地事务已经回滚。
*/
final String orderId = messageView.getProperties().get("OrderId");
if (Strings.isNullOrEmpty(orderId)) {
// 错误的消息,直接返回Rollback。
return TransactionResolution.ROLLBACK;
}
return checkOrderById(orderId) ? TransactionResolution.COMMIT : TransactionResolution.ROLLBACK;
})
.build();
//开启事务
final Transaction transaction;
try {
transaction = producer.beginTransaction();
} catch (ClientException e) {
e.printStackTrace();
//事务开启失败,直接退出。
return;
}
Message message = messageBuilder.setTopic("topic")
//设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
//设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("messageTag")
//一般事务消息都会设置一个本地事务关联的唯一ID,用来做本地事务回查的校验。
.addProperty("OrderId", "xxx")
//消息体。
.setBody("messageBody".getBytes())
.build();
//发送半事务消息
final SendReceipt sendReceipt;
try {
sendReceipt = producer.send(message, transaction);
} catch (ClientException e) {
//半事务消息发送失败,事务可以直接退出并回滚。
return;
}
/**
* 执行本地事务,并确定本地事务结果。
* 1. 如果本地事务提交成功,则提交消息事务。
* 2. 如果本地事务提交失败,则回滚消息事务。
* 3. 如果本地事务未知异常,则不处理,等待事务消息回查。
*
*/
boolean localTransactionOk = doLocalTransaction();
if (localTransactionOk) {
try {
transaction.commit();
} catch (ClientException e) {
// 业务可以自身对实时性的要求选择是否重试,如果放弃重试,可以依赖事务消息回查机制进行事务状态的提交。
e.printStackTrace();
}
} else {
try {
transaction.rollback();
} catch (ClientException e) {
// 建议记录异常信息,回滚异常时可以无需重试,依赖事务消息回查机制进行事务状态的提交。
e.printStackTrace();
}
}
}

注意事项

  • 幂等性: 消费者处理消息时需要确保业务逻辑的幂等性,以应对消息可能的重复消费。
  • 超时和监控: 设置合理的超时时间,并监控事务消息的性能

总结

RocketMQ 事务消息是分布式事务中一种常见的实现方案,只是把发送消息和本地事务放在一个事务中,并且只保证最终一致性,无法保证强一致性。

原因有两点:

  1. 执行完成本地事务后,在commit事务消息之前,这段时间内数据是不一致的,所以只是保证了发送消息和本地事务的最终一致性。
  2. 在commit事务消息之后,然后把消息投递给消费者。至于消费者是否消费消息,什么时候消费?也都是不可控的,所以也只能尽量保证数据最终一致性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

观察SwiftUI ScrollView的内容偏移量 观察S

发表于 2024-04-26

观察SwiftUI ScrollView的内容偏移量

hudson 译 原文

在构建各种可滚动UI时,通常希望观察当前滚动位置(UIScrollView称之为内容偏移),以触发布局更改,在需要时加载其他数据,或根据用户当前正在查看的内容执行其他类型的操作。

然而,当涉及到SwiftUI的ScrollView时,目前(在撰写本文时)没有内置的方式来执行此类滚动观察。虽然在滚动视图中嵌入ScrollViewReader确实能够在代码中的更改滚动位置,但奇怪的是(特别是考虑到它的名称),它不允许我们以任何方式读取当前(滚动)内容偏移量。

解决这个问题的一种方法是利用UIKit的UIScrollView的丰富功能,由于其代理协议和scrollViewDidScroll方法,它提供了一种在发生任何类型的滚动时获得通知的简单方法。然而,尽管我通常非常喜欢使用UIViewRepresentable和其他 SwiftUI/UIKit互操作性机制,但在这种情况下,我们必须编写相当多的额外代码来弥合两个框架之间的差距。

这主要是因为——至少在iOS上——我们只能将SwiftUI内容嵌入到UIHostingController中,而不是在自我管理的UIView中。因此,如果我们想使用UIScrollView构建一个自定义的、可观察的ScrollView版本,那么我们必须将该实现包装在视图控制器中,然后管理我们的UIHostingController与键盘、滚动视图的内容大小、安全区域嵌入等之间的关系。虽然不是不可能,但仍然有相当多的额外工作和复杂性。

因此,让我们看看是否能找到一种完全SwiftUI原生的方式来执行此类内容偏移观测。

使用GeometryReader解析框架

在开始之前,要意识到的关键一点是,UIScrollView和SwiftUI的ScrollView都通过修改一个容器偏移执行滚动,该容器托管实际可滚动内容。然后,他们将该容器裁剪到边界上,以产生视口移动的错觉。因此,如果能找到观察该容器框架(frame)的方法,那么基本上也会找到观察滚动视图内容偏移的方法。

这就是我们的老朋友GeometryReader发挥作用的地方(没有它,就不会是一个合适的SwiftUI布局解决方法,对吗?)。虽然GeometryReader主要用于访问它所托管的视图的size大小(或者更准确地说,该视图的拟议大小),但它还有另一个巧妙的技巧——可以要求它读取相对于给定坐标系的当前视图的frame框架。

要使用该功能,让我们从创建一个PositionObservingView开始,它允许将CGPoint值绑定到该视图相对于 CoordinateSpace的当前位置,我们也将将其作为参数传递。然后,新视图将嵌入一个GeometryReader作为背景(这将使该GeometryReader具有与视图本身相同的大小),并将使用首选项键将已经解析的框架的origin原点设置为偏移量——像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
swift复制代码struct PositionObservingView<Content: View>: View {
var coordinateSpace: CoordinateSpace
@Binding var position: CGPoint
@ViewBuilder var content: () -> Content

var body: some View {
content()
.background(GeometryReader { geometry in
Color.clear.preference(
key: PreferenceKey.self,
value: geometry.frame(in: coordinateSpace).origin
)
})
.onPreferenceChange(PreferenceKey.self) { position in
self.position = position
}
}
}

要了解如何使用@ViewBuilder属性构建自定义SwiftUI容器视图的更多信息,请查看这篇文章。

上面代码中使用SwiftUI的首选项系统的原因是,GeometryReader将作为视图更新过程的一部分被调用,在此过程中,不允许直接改变视图的状态。因此,通过使用首选项,我们可以以异步方式将CGPoint值传递到视图,然后将这些值分配给position 绑定变量。

现在,需要做的就是实现上面使用的PreferenceKey类型,如下:

1
2
3
4
5
6
7
8
9
swift复制代码private extension PositionObservingView {
struct PreferenceKey: SwiftUI.PreferenceKey {
static var defaultValue: CGPoint { .zero }

static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
// No-op
}
}
}

实际上不需要实现上述任何类型的reduce算法,因为只有单个视图在任何给定的层次结构中使用该首选项键的传递值(因为上述实现完全包含在PositionObservingView中)。

好吧,现在有了一个能够读取和观察自己在给定坐标系中位置的视图。让我们使用该视图构建一个ScrollView包装器, 实现我们的原始目标——能够在此类滚动视图中读取当前内容偏移量。

从位置到内容偏移

新的ScrollView包装器基本上将有两个职责——一,它需要将内部PositionObservingView的位置转换为当前滚动位置(或内容偏移),二,它还需要定义一个CordinateSpace坐标空间,内部视图可以使用它来解析其位置。除此之外,它只需将其配置参数转发到其底层的ScrollView, 以决定滚动视图在哪些轴上运行,以及是否显示滚动指示器。

好消息是,将内部视图的位置转换为内容偏移就像给CGPoint的x和y分量取负一样简单。这是因为,如前所述,滚动视图的内容偏移量本质上只是容器相对于滚动视图边界移动的距离。

因此,继续实施我们的自定义滚动视图,将其命名为OffsetObservingScrollView(这种情况下拼写出ContentOffset确实有点太冗长了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
swift复制代码struct OffsetObservingScrollView<Content: View>: View {
var axes: Axis.Set = [.vertical]
var showsIndicators = true
@Binding var offset: CGPoint
@ViewBuilder var content: () -> Content

// The name of our coordinate space doesn’t have to be
// stable between view updates (it just needs to be
// consistent within this view), so we’ll simply use a
// plain UUID for it:
private let coordinateSpaceName = UUID()

var body: some View {
ScrollView(axes, showsIndicators: showsIndicators) {
PositionObservingView(
coordinateSpace: .named(coordinateSpaceName),
position: Binding(
get: { offset },
set: { newOffset in
offset = CGPoint(
x: -newOffset.x,
y: -newOffset.y
)
}
),
content: content
)
}
.coordinateSpace(name: coordinateSpaceName)
}
}

注意我们如何通过使用闭包定义getter和setter,为内部视图的位置参数创建完全自定义的绑定。如上述情况,在将一个值分配给另一个绑定之前需进行转换时,自定义的绑定是一个很好的选择。

就是这样!现在有了一个SwiftUI内置ScrollView的临时替代品,它能够观察当前内容偏移量——可以将其绑定到任何状态属性,例如更改头部视图的布局,向服务器报告分析事件,或执行任何其他类型的基于滚动位置的操作。你可以在这里找到一个使用上述OffsetObservingScrollView的完整示例,以实现可折叠的头部视图。

我希望你发现这篇文章有趣且有用。如果有任何问题、评论或反馈,请随时在Mastodon 上找到我,或通过电子邮件与我联系。

感谢您的阅读!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SwiftUI视图与修饰符 SwiftUI视图与修饰符

发表于 2024-04-26

SwiftUI视图与修饰符

hudson 译 原文

至少从架构的角度来看,SwiftUI最有趣的方面之一是它本质上如何将视图视为数据。毕竟,SwiftUI视图不是屏幕上渲染的像素的直接表示,而是对给定UI应该如何工作、外观和行为的描述。

在如何构建视图代码时,这种非常数据驱动的方法给了我们很大的灵活性——以至于人们甚至可能会开始质疑将一个UI定义为视图类型与将相同的代码作为修饰符实现之间到底有什么区别。

以下面FeaturedLabel视图为例——它在将星形图片添加到给定文本前面,并应用特定的前景色和字体,使该文本脱颖而出,成为“特色”:

1
2
3
4
5
6
7
8
9
10
11
12
swift复制代码struct FeaturedLabel: View {
var text: String

var body: some View {
HStack {
Image(systemName: ”star“)
Text(text)
}
.foregroundColor(.orange)
.font(.headline)
}
}

虽然上述内容可能看起来像典型的自定义视图,但使用“修改器风格”的View 扩展,也可以轻松实现完全相同的界面效果——像这样:

1
2
3
4
5
6
7
8
9
10
swift复制代码extension View {
func featured() -> some View {
HStack {
Image(systemName: ”star“)
self
}
.foregroundColor(.orange)
.font(.headline)
}
}

当放在示例ContentView中时,这两个不同的解决方案并排看起来如下:

1
2
3
4
5
6
7
8
9
10
11
swift复制代码struct ContentView: View {
var body: some View {
VStack {
// View-based version:
FeaturedLabel(text: ”Hello, world!“)

// Modifier-based version:
Text(”Hello, world!“).featured()
}
}
}

两种解决方案之间的一个关键区别是,后者可以应用于任何视图,而前者只允许我们创建基于String的特色标签。不过,我们可以通过将我们的FeaturedLabel转换为自定义容器视图来解决这个问题,该视图接受任何符合视图的内容,而不仅仅是一个普通的字符串:

1
2
3
4
5
6
7
8
9
10
11
12
swift复制代码struct FeaturedLabel<Content: View>: View {
@ViewBuilder var content: () -> Content

var body: some View {
HStack {
Image(systemName: ”star“)
content()
}
.foregroundColor(.orange)
.font(.headline)
}
}

上面,我们将ViewBuilder属性添加到 content闭包中,便可以利用SwiftUI的视图构建API的全部功能(例如, 可以为每个FeaturedLabel构建内容时使用if和switch语句)。

不过,我们可能仍然希望使用一个字符串轻松初始化 FeaturedLabel 实例,而不是总是传递包含Text视图的闭包。谢天谢地,使用类型约束,可以轻松实现:

1
2
3
4
5
6
7
swift复制代码extension FeaturedLabel where Content == Text {
init(_ text: String) {
self.init {
Text(text)
}
}
}

上图中,我们使用下划线来删除文本的外部参数标签,以模仿SwiftUI自己的内置便利API,如Button和NavigationLink等类型工作的方式。

有了这些改变,两种解决方案现在都具备完全相同的灵活性,并且既可以轻松地用于创建基于文本的标签,也可渲染任何类型的SwiftUI视图内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
swift复制代码struct ContentView: View {
@State private var isToggleOn = false

var body: some View {
VStack {
// Using texts:
Group {
// View-based version:
FeaturedLabel(”Hello, world!“)

// Modifier-based version:
Text(”Hello, world!“).featured()
}

// Using toggles:
Group {
// View-based version:
FeaturedLabel {
Toggle(”Toggle“, isOn: $isToggleOn)
}

// Modifier-based version:
Toggle(”Toggle“, isOn: $isToggleOn).featured()
}
}
}
}

此时我们可能会真正开始问自己——将UI定义为视图与修饰符到底有什么区别? 除了代码风格和结构外,真的有什么实际的差异吗?

嗯,那状态呢? 假设我们想让新的特色标签在首次出现时自动淡入? 这需要我们用@State-来标记opacity属性,然后在onAppear闭包中实现动画效果——例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
swift复制代码struct FeaturedLabel<Content: View>: View {
@ViewBuilder var content: () -> Content
@State private var opacity = 0.0

var body: some View {
HStack {
Image(systemName: ”star“)
content()
}
.foregroundColor(.orange)
.font(.headline)
.opacity(opacity)
.onAppear {
withAnimation {
opacity = 1
}
}
}
}

起初,参与SwiftUI状态管理系统似乎只有适当的视图类型才能做到,但事实证明,修饰符具有完全相同的功能——只要我们将此类修饰符定义为适当的符合 ViewModifier协议的类型,而不仅仅是使用View协议扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
swift复制代码struct FeaturedModifier: ViewModifier {
@State private var opacity = 0.0

func body(content: Content) -> some View {
HStack {
Image(systemName: ”star“)
content
}
.foregroundColor(.orange)
.font(.headline)
.opacity(opacity)
.onAppear {
withAnimation {
opacity = 1
}
}
}
}

有了上述功能,我们现在可以用调用将新的 FeaturedModifier 添加到当前视图中来,取代之前的featured方法实现——两种方法再次产生完全相同的最终结果:

1
2
3
4
5
swift复制代码extension View {
func featured() -> some View {
modifier(FeaturedModifier())
}
}

同样值得注意的是,在ViewModifier类型中包装代码时,代码是延迟加载的,即在需要时计算代码,而不是在首次添加修饰符时就执行的,某些情况下,性能可能会有所不同。

因此,无论我们是想更改视图的样式或结构,还是引入新的状态,SwiftUI视图和修饰符都具有完全相同的功能。这点开始变得明显。但这让我们想到了下一个问题——如果这两种方法之间没有实际差异,我们如何在它们之间做出选择?

至少对我来说,这一切都归结为结果视图层次结构。虽然从技术上讲,在HStack中包装一个特色标签改变了视图层次结构,以添加星形图像,但从概念上讲,这更多的是关于样式,而不是结构。当将featured修饰符应用于视图时,从任何有意义方式说上,其布局或在视图层次结构中的位置并没有真正改变 ——至少从高级的角度来看,它仍然只是具有完全相同整体布局的单一视图。

不过,情况并非总是如此。看看另一个例子,它应该更清楚地说明视图和修饰符之间的潜在结构差异。

在这里,我们编写了一个SplitView容器,它采用两个视图——一个是前导视图,一个是尾随视图——然后将它们并排渲染,中间是一个分隔符控件,同时最大化其frame,最终它们将平均分隔可用空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
swift复制代码struct SplitView<Leading: View, Trailing: View>: View {
@ViewBuilder var leading: () -> Leading
@ViewBuilder var trailing: () -> Trailing

var body: some View {
HStack {
prepareSubview(leading())
Divider()
prepareSubview(trailing())
}
}

private func prepareSubview(_ view: some View) -> some View {
view.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

就像以前一样,绝对可以使用基于修饰符的方法实现完全相同的结果——这可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
swift复制代码extension View {
func split(with trailingView: some View) -> some View {
HStack {
maximize()
Divider()
trailingView.maximize()
}
}

func maximize() -> some View {
frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

然而,如果再次将两个解决方案放在同一个示例ContentView中,那么可以看到,这次这两种方法在结构和清晰度方面看起来确实大不相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
swift复制代码struct ContentView: View {
var body: some View {
VStack {
// View-based version:
SplitView(leading: {
Text(”Leading“)
}, trailing: {
Text(”Trailing“)
})

// Modifier-based version:
Text(”Leading“).split(with: Text(”Trailing“))
}
}
}

看看上面的基于视图的调用方法,很明显,两个文本被包装在一个容器中,也很容易判断哪个是前导视图,哪个是尾随视图。

不过这次,不能对基于修饰符的版本说同样的事情,我们确实需要知道,应用修饰符的视图 是前导视图。此外,也无法判断这两个文本最终会被包裹在什么样的容器中。看起来更像是使用尾随标签以某种方式为前导标签增加某种风格 ,但事实并非如此。

虽然可以尝试用更详细的API命名来解决清晰度问题,但核心问题仍然存在——在这种情况下,基于修饰符的版本没有正确显示结果的视图层次结构。因此,在上述情况下,当我们在父容器中包装多个兄弟姐妹时,选择基于视图的解决方案通常会给我们一个更清晰的最终结果。

另一方面,如果我们所做的只是将一组样式应用于单个视图,那么将其实现为“修改器类似”的扩展,或使用正确的ViewModifier类型,将是最常要走的路。对于介于两者之间的一切——例如我们早期的“特色标签”示例——这一切都归结为代码风格和个人偏好,即哪种解决方案最适合每个给定项目。

看看SwiftUI的内置API是如何设计的——容器(如HStack和VStack)是视图,而样式API(如填充和前景颜色)是作为修饰符实现的。因此,如果在项目中尽可能多地遵循相同的方法,那么最终UI代码可能会感觉一致,且与SwiftUI本身一致。

我希望你发现这篇文章有趣且有用。如果有任何问题、评论或反馈,请随时在Mastodon 上找到我,或通过电子邮件与我联系。

感谢您的阅读!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

代码报错不用愁,CodeGeeX一键完成代码修复、错误解释的

发表于 2024-04-26

作为一名开发者,你一定遇到过在编写代码时出现的各种错误。这些错误可能是语法错误、运行时错误或者逻辑错误。处理这些错误通常需要花费大量的时间和精力,特别是当你对错误的原因一无所知时。

CodeGeeX的v2.7.4版本最新上线的代码修复和错误解释功能,让你在解决代码错误的问题上,变得更加简单和高效。下面我们详细介绍这个功能的用法和适用场景,快去更新插件体验起来吧!

一、直接在编辑器中修复代码错误

当你在VSCode、JetBrains全家桶的IDE代码编辑器中编写代码时,如果出现了错误,编辑器通常会用红线来标注出错的代码行。

使用CodeGeeX,你不再需要手动查找错误的原因或者翻阅文档来寻找解决方案。如果你使用的VSCode上的CodeGeeX插件,只需要在出现错误的代码行上点击鼠标右键,选择“使用CodeGeeX修复”的选项,CodeGeeX将自动分析错误,并提供修复建议。

如果你是使用了IDEA,在出现错误的红线代码处,先点击more actions,然后选择fix by codegeex,如下图所示:

视频图1.png

视频图2.png

二、灯泡图标中的CodeGeeX修复功能

除了右键菜单,CodeGeeX还增强了编辑器中的灯泡图标功能。当你点击灯泡图标时,除了编辑器自带的功能选项外,还会看到“使用CodeGeeX修复”的选项。这意味着你可以在不离开当前编辑环境的情况下,直接利用CodeGeeX来修复代码错误。

视频图3.png

三、终端运行时报错的智能解释

在终端运行代码时,经常会遇到各种报错信息。这些信息有时可能非常复杂,难以理解。现在,当你在终端遇到错误时,只需通过点击右键菜单命令“使用CodeGeeX解释”(Windows需要Shift+右键)。

IDEA只需点击终端报错行旁边的按钮,就可以让CodeGeeX来解释这些错误。CodeGeeX能够理解错误信息,并提供清晰、易于理解的解释,甚至还能给出修复步骤。这使得即使是新手开发者也能快速理解并解决问题。

高效的开发不仅仅是写代码,更包括如何快速、优雅地解决问题。

四、更多交互优化,提升开发效率

JetBrains全家桶v2.7.4版本后,还有哪些值得关注的功能更新,下面以IDEA为例,一图看懂在IDEA的侧边栏,还哪些便捷的智能操作。

图1.png

侧边栏顶部的“更多”下增加”设置“菜单入口。类似于VSCode,CodeGeeX在JetBrains IDEs平台上新增了顶部的设置菜单入口,用户可以更轻松的打开设置,自定义插件的行为更符合自己的使用习惯。

图2.png

如上图所示,在CodeGeeX侧边栏上方,点击”…”,弹出下拉菜单,点击“设置”进入页面。

图3.png

侧边栏智能问答提供更多直观操作代码的方式。在智能问答内容生成框上方的“更多”中,提供了将生成代码插入到新文件、终端中运行、与当前文件对比、与剪贴板对比、折行显示等多项操作的功能。

图4.png

智能问答支持一键复制。在侧边栏使用Ask CodeGeeX智能问答时,需要同时复制生成的代码和文本内容。CodeGeeX提供一键复制所有文本内容的按钮,并且使用Markdown格式整理来方便使用。

图5.png

预测后续问题。在智能问答Ask CodeGeeX中,当用户提出一个问题获得回复后,会继续生成接下来的后续问题。

图6.png

这些推荐给用户的后续问题,是和用户提出的问题相关性很强或者更进一步的问题预测。

图7.png

通过检索算法的优化,新版本中的@repo效果明显提升。同时,根据用户反馈,新增更多开源代码仓库的支持。(点击‘@repo’图标后,输入仓库名即可找到)

图8.png

图9.png

CodeGeeX收录的流行开源仓库已经超过100+个,在输入代码仓库名称的同时,展示出的开源仓库列表会根据检索收录结果发生变化。

Diff视图下新增代码审查功能和自动生成Commit Message的功能

图10.png

如果你对CodeGeeX的这一波新功能感兴趣,一定要去IDE的插件市场更新最新版的CodeGeeX插件来使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「KMM」、「KMP」?他们到底是什么?

发表于 2024-04-26

Kotlin Multiplatform is a technology that allows you to create applications for various platforms and efficiently reuse code across them while retaining the benefits of native programming. Your multiplatform applications will work on different operating systems, such as iOS, Android, macOS, Windows, Linux, and others. —— kotlin官网

以上是Kotlin Multiplatform官网上对于KMP的简要概括,翻译一下,就是

“Kotlin Multiplatform 是一种跨平台技术,它允许您在不同平台上高效的重用代码的同时保留不同平台的代码优势。使用KMP编写的程序可以在不同的操作系统上运行,例如iOS、Android、macOS、Windows、Linux等”。

image.png

KMM?KMP?

KMM(Kotlin Multiplatform Mobile)和KMP(Kotlin Multiplatform)实际上是指同一个概念在不同范围的应用。Kotlin Multiplatform是一个更广泛的术语,而Kotlin Multiplatform Mobile专注于移动开发。

Kotlin Multiplatform (KMP)

Kotlin Multiplatform是一种编程技术,允许开发者使用Kotlin语言编写跨平台代码。这意味着你可以用同一套代码逻辑同时运行在多个平台上(如JVM、JavaScript、iOS、Android、Linux、Windows、MacOS等),而不需要为每个平台重写逻辑。

KMP的目标是实现业务逻辑或非UI相关逻辑的共享,以减少重复代码和提高开发效率。对于UI部分,通常还是需要使用各个平台的原生技术或其他跨平台框架。

KMP的应用范围非常广泛,包括但不限于移动应用开发。它也可以用于Web开发、服务器端开发、桌面应用开发等。

Kotlin Multiplatform Mobile (KMM)

KMM是Kotlin Multiplatform在移动开发领域的应用。它专注于使用Kotlin编写可以在Android和iOS平台上共享的代码。

与KMP的差异主要体现在使用范围上,KMM专门针对移动平台,即iOS和Android(也许,maybe未来也包括鸿蒙?)

Kotlin Multiplatform(无论是KMM还是更广泛的KMP)提供了一种高效的方式来共享跨平台代码,同时允许开发者为每个平台提供最佳的用户体验,如果你的项目只涉及到iOS和Android应用的开发,KMM是一个很好的选择,因为它专为这个目的设计。如果你希望在更广泛的平台上共享Kotlin代码(如Web、桌面或服务器端),那么KMP会是更适合的选择。

受命于天,既寿永昌:谁才是跨端正统?

打盘古开天辟地以来,人类对统一一直有一种莫名的执念:非我族类,其心必异。所以历史上各路英雄疯了似的都要统一天下。
前端开发亦是如此:明明都是UI切图仔,为何还要区分出Android切图仔、iOS切图仔、web切图仔…为了达成一统天下的目的,前端历史上也是英雄辈出:React Native、Weex、Flutter以及我们此次要讲的KMM(KMP)。

一个国家,三个政府,这难道不是分裂,不是对孙先生的背叛吗?—— 《建军大业》

下面的表格列举出了几个主流的跨端方案的差异性:

特征/技术 Weex React Native Flutter Kotlin Multiplatform (KMP)
开发者/维护者 阿里巴巴 Meta (前Facebook) Google JetBrains
技术栈 Vue.js, JavaScript JavaScript, React Dart Kotlin
UI渲染 原生组件 原生组件 自绘UI(通过Skia) 依赖于平台
性能 接近原生 接近原生 高(性能更优) 取决于平台,逻辑部分高效
跨平台范围 iOS, Android iOS, Android, Web (部分支持) iOS, Android, Web, Desktop, Embedded iOS, Android, Web, Desktop
学习曲线 适中,需要了解Vue.js 适中,需要了解React 较陡峭,需要学习Dart语言 适中,对Kotlin开发者较为友好
社区和支持 适中 强大 强大 增长中
用例 适合阿里生态圈内的项目 广泛的应用场景,尤其是需要快速迭代的项目 适合高性能和高度定制化的UI设计的应用 适合逻辑重用,跨多平台共享业务逻辑

其中React Native、Flutter成名已久,也算是跨端方案两种实现思路的代表:

  • 以React Native为代表的使用平台的原生组件来构建UI。这就意味着它需要讲JavaScript代码桥接到原生代码,使用原生控件来渲染界面。这种方法可以提供接近原生的性能和外观,但可能会受限于框架对原生组件的支持。与之类似的还有Weex。
  • 以Flutter为代表的使用自己的渲染引擎绘制界面、构建UI。这种方案由于避免了桥接开销,通常可以提供更好的性能,使得动画和过渡更加平滑,响应更快。与之类似的还有Compose Multiplatform。

React Native虽然号称可以实现接近原生的性能,但由于其天然存在的桥接机制(JavaScript与原生代码之间的通信),对于一些要求高性能的应用或复杂动画,React Native可能无法与原生应用相匹敌。
同时,尽管React Native旨在支持跨平台开发,但在实际开发中,仍然可能需要编写平台特定的代码来处理不同操作系统之间的差异。这可能减少代码复用率,增加开发和维护成本。

至于Flutter呢,性能上倒是和原生没有太大差异,对于那些不熟悉Dart语言的开发者来说,Flutter可能有一定的学习曲线。虽然Dart设计为易于学习的语言,但它并不是什么大众性的语言。开发者往往需要额外的时间和资源来学习Dart语言及Flutter框架。

同时,虽然Flutter努力提供丰富的插件来支持各种平台特定功能,但新的平台功能或较少使用的功能可能没有现成的插件支持,这些也需要开发者自行编写原生代码并创建桥接逻辑,无疑增加了开发的复杂性和时间成本。

什么?你问我「Weex」?

大人,时代变了,大清亡了啊!Weex的Github最近的一次提交是在23年8月份[/哭]

「KMP」新王当立?

更细粒度、渐进式的跨平台

image.png

与传统的Flutter or React Native方案不同,KMP对于跨平台代码的粒度更加精细,允许开发者 渐进式 的完成跨平台,按代码的重用程度不同分为3种:

  • 共享部分逻辑层代码

在保障app的稳定性的前提下,将部分关键且独立的逻辑层代码进行共享,使其能在不同的平台上重用这部分的代码。这种方式适合现有项目向KMP的迁移,最大程度的保证app的稳定性。

  • 逻辑层代码完全跨平台,UI层代码保持平台独立性

逻辑层代码完全实现跨平台,但是UI部分的代码保持平台独立性。这种模式比较适合新项目,没有历史债务,从新开始。

  • 逻辑层和UI层完全的跨平台,即代码100%重用

跨平台项目的完全体,UI部分使用Compose Multiplatform来完成跨平台达到重用代码的目的。
使用Compose Multiplatform编写的跨平台项目

性能有保障的跨平台

通过KMP跨平台而共享的代码,最终会被编译成对应平台的二进制文件。

什么意思呢?我们使用Kotlin写的跨平台的代码,最终编译生成的,不一定是.class文件了,可能是js文件、.exe、.framwork…等等都有可能 —— 这取决于目标平台。

这个特性天然就决定了KMP的性能相对于React Native和Flutter更好。理论上KMP生成的代码和原生代码在性能上应该别无二致。

image.png

说的天花乱坠,都谁在用KMP?

官网上列举了很多案例:

基本上以国外公司为主,不过据我所知,国内的很多公司也已经在生产环境上尝试KMP,像小破站、莉莉丝、美团等。

Google也一直在加大对KMP的支持力度,早在2022年10月份就宣布了Jetpack开始支持KMP了:

Announcing an Experimental Preview of Jetpack Multiplatform Libraries

Jetpack开始支持KMP

所以对于KMP的社区生态或者说是开发体验上,我是保持着相对乐观的态度:背靠Google以及最会做IDE的Jetbrains,我相信KMP的生态不会差,时间问题罢了。

该不该使用KMP?

开源的世界百家争鸣,但是落到实处往往也是成王败寇。作为开发者,做技术选型就更需要慎重再慎重了。
如果你有跨平台的诉求,我认为KMP当前值得推荐。

作为一个Android开发者、Kotlin语言的使用者,Kotlin、Compose本身就很熟悉,使用KMP几乎没什么学习成本和门槛,跨平台的能力简直算是白送的。即便不熟悉Compose,我们也可以使用KMM写一些UI无关的、逻辑层的代码,共用逻辑层,达到跨平台的能力。比如使用KMP做埋点数据上报。

至于跨平台是不是伪需求,我觉得应该高瞻远瞩一下。现有主流的移动端平台只有iOS和Android,加上未来的「鸿蒙」呢?之前一套逻辑iOS和Android平台上写两次,现在加上鸿蒙写三遍么?即使你能忍,公司能不能忍呢?KMP对人效的提升和逻辑的统一性我觉得是无需质疑的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mac下SourceTree导出更新文件

发表于 2024-04-26

SourceTree可视化git管理工具功能很强大,支持自定义操作可以写脚本,现在教大家做一个导出更新文件的脚本吧!

添加自定义操作

截屏2024-04-26下午3.40.32.png

选择自定义文件导出

截屏2024-04-26下午3.42.33.png

sourcetree_exports_file.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sh复制代码#!/bin/bash
#仓库路径
REPO_PATH=$1
#导出文件的目录以及新建文件夹路径
FOLDER="/Users/nightkid/Downloads"
FOLDER_DATE=`date +%Y%m%d_%H_%M_%S`
PROJECT_NAME=${REPO_PATH##*/}
for arg in "$@"
do
if [ $1 != $arg ]
then
srcfile="$REPO_PATH/$arg"
echo $arg
cpfile="$FOLDER/${PROJECT_NAME}_$FOLDER_DATE/$arg"
folder=${cpfile%/*}
`mkdir -p $folder`
result=`cp -f $srcfile $cpfile`
echo "$arg"
fi
#打开导出文件夹
open $FOLDER/${PROJECT_NAME}_$FOLDER_DATE
done

选择版本号内容导出文件

截屏2024-04-26下午3.42.14.png

sourcetree_commit_exports.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
sh复制代码#!/bin/bash
REPO_PATH=$1
GIT_SHA=$2
#导出文件保存路径
FOLDER="/Users/nightkid/Downloads"
FOLDER_DATE=`date +%Y%m%d_%H_%M_%S`
PROJECT_NAME=${REPO_PATH##*/}
`mkdir -p $FOLDER`
filepath=$(cd $REPO_PATH; pwd)
split_str=`git show --format= --name-status $GIT_SHA`
echo "$split_str" > "$FOLDER/tmp.txt"

split_array=()
split_len=0
while read line
do
split_array[$split_len]=$line
split_len=$split_len+1

done < "$FOLDER/tmp.txt"

for ((i=0;i<${split_array[@]};i++));do
{
string=${split_array[$i]}

array=(${string//,/ })
if [ "$array[0]" != "D" ]
then
echo ${array[1]}

srcfile="$REPO_PATH/${array[1]}"
cpfile="$FOLDER/${PROJECT_NAME}_$FOLDER_DATE/${array[1]}"
folder=${cpfile%/*}
`mkdir -p $folder`
`cp -f $srcfile $cpfile`
fi
# for var in ${array[@]}
# do
# echo $var
# done
}
open $FOLDER/${PROJECT_NAME}_$FOLDER_DATE
done

最后选择对应的版本、文件右键

截屏2024-04-26下午3.47.58.png

可能的错误

1
2
3
4
5
6
bash复制代码当提示一下错误: 
launch path not accessible Completed with errors, see above

您可以通过授予您的用户执行该文件的权限来解决此问题:

chmod +x your-script.sh

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在 GPU 云上跑 LLama3

发表于 2024-04-26

LLama 3 模型已经开源了,感觉有一大波 Chinese -LLama 3 正在赶来的路上。如果你也想基于 LLama 3 训练一个自己的模型,那这篇教程就教你怎么来做。

在本文中,我们将介绍LLama 3,这是下一代最先进的开源大型语言模型。我们将了解LLama 3相对于LLama 2的进步。然后,我们将利用 Paperspace GPU 云的能力来深入探索,并尝试上手这个模型。因为 Paperspace 上有包括 H100、A100 等一系列 GPU 资源可以使用。

简单聊聊 LLama3

这里给还未了解 LLama 3 的开发者们,简要地介绍一下它。如果你已经是 AI 圈的老手,那么可以跳过这个章节。

Meta 最近宣布了LLama 3,这是下一代最先进的开源大型语言模型。

LLama 3 现在拥有 8B(80亿)和 70B(700亿)参数的语言模型。该模型已在各种任务中都有不俗的表现,并提供更好的推理能力。该模型已经开源,可供商业使用,并且开发者们可以在 AI 应用、开发者工具等方面进行创新。

LLama 3 有四个版本的大型语言模型(LLM)。这些模型有两种参数规模:8B 和 70B 参数,每种都有基础(预训练)和 instruct-tuned 版本。它们可以在不同类型的消费级硬件上平稳运行,并支持 8K(8000)token 的上下文长度。

  • Meta-Llama-3-8b:基础 8B 模型
  • Meta-Llama-3-8b-instruct:基于 8B 模型的 instruct-tuned 版本
  • Meta-Llama-3-70b:基础 70B 模型
  • Meta-Llama-3-70b-instruct:基于 70B 模型的 instruct-tuned 版本

LLama 3 的增强

最新的 8B 和 70B 参数的 LLama 3 模型,相比 LLama 2 有显著进步。有一些人表示,这是为大型语言模型设定了新的标准。由于更好的预训练和微调方法,它们已成为同类模型中的顶级存在。后训练增强中错误明显减少了,并提高了模型在推理、生成代码和遵循指令方面的性能。简而言之,LLama 3 比之前的很多模型都更先进、更灵活。下图是源自 Meta 官方的数据。

图:LLama 3 性能基准

在开发 LLama 3时,主要关注点是模型在现实生活情境中的优化。为此,他们创建了一个评估集,包含 1800 个 prompt,涵盖 12 个关键任务:寻求建议、编码和总结。此外,验证数据集也被禁止研发团队访问,以防止模型过拟合。将 LLama 3 与 Claude Sonnet、Mistral Medium 和 GPT-3.5 进行人工评估后,发现它在各种任务和场景中的结果都有不错的表现。

图:显示了人工评估在几个类别和提示下的结果

为了增强 LLama 3 的能力,Meta 专注于扩大预训练规模和完善后训练技术。

他们扩大了预训练规模,并制定了一系列详细的 scaling laws,以优化计算性能。令人惊讶的是,即使在训练了大量数据——高达 15T(万亿) 个 token 之后——其性能仍呈对数线性增长。结合使用各种并行化方法和定制的 GPU 集群,与LLama 2 相比,训练效率有效提高了三倍。

对于指令微调,Meta 考虑了不同的技术,如监督微调和偏好优化。此外,详细制定了训练数据和从偏好排名中学习,这有效提高了模型的性能,特别是在推理和编码任务中。这些改进使模型能够更好地理解和响应复杂任务。

模型架构

在设计 LLama 3 时,采用标准的解码器只有变换器架构,优化了编码效率和推理效率。与LLama 2相比,采用了有 128K 个标记词汇表的分词器,能更有效地对语言进行编码。此外,为了在推理期间让 LLama 3 模型更快,还引入了不同大小的分组查询注意力(GQA)。在训练期间,使用了 8192 个 token 的序列和一种掩码技术,以保持文档边界内的注意力。

LLama 3 在超过 15T token 的公开数据集上进行了预训练——比 LLama 2 使用的数据大 7 倍,而且代码量也大了 4 倍。该模型包含超过 5% 的非英语数据,涵盖 30 种语言,以便实现支持多语言。

为了保持在高质量的数据上进行训练,Meta 还构建了一系列数据过滤管道,还使用了启发式过滤器和文本分类器等,目的就是为了提高了模型性能。

运行 LLama 3 Demo

在我们开始之前,请确保在 huggingface.co 上获得对“meta-llama/Meta-Llama-3-70B”模型的访问权限。另外,我们在这里使用的是 Paperspace 平台上的 GPU,如果你手上还没有合适的 GPU 和机器,可以考虑这个平台。

要使用 Llama 3,我们首先要升级 transformers 包。

1
2
lua复制代码#upgrade the transformer package
pip install -U "transformers==4.40.0" --upgrade

接下来,运行以下代码段。根据 Hugging Face 博客的提示,该模型通常需要大约 16GB 的 RAM,包括像 3090 或 4090 这样的 GPU。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码import transformers
import torch

model_id = "meta-llama/Meta-Llama-3-8B-Instruct"

pipeline = transformers.pipeline(
"text-generation",
model="meta-llama/Meta-Llama-3-8B-Instruct",
model_kwargs={"torch_dtype": torch.bfloat16},
device="cuda",
)

pipeline("Hey how are you doing today?")

如果你收到错误信息 “RuntimeError: cutlassF: no kernel found to launch!”,请尝试下面的代码,并再次运行该cell。

1
2
python复制代码torch.backends.cuda.enable_mem_efficient_sdp(False)
torch.backends.cuda.enable_flash_sdp(False)

生成的文本:“Hey how are you doing today? I hope you’re having a great day so far! I just”

在这里需要注意几件事情:

  • 在我们的示例案例中,我们使用了’bfloat16’来加载模型。最初,Meta 使用的是’bfloat16’。因此,这是一种官方推荐的运行方式,可以确保最佳精度或进行评估。当然,你也可以尝试使用 float16,根据你硬件配置的情况,这可能会更快。
  • 你还可以自动压缩模型,将其加载为 8 位或 4 位模式。在 4 位模式下运行需要的内存更少,使其能兼容许多消费级GPU 和性能较弱的 GPU。以下是如何以4位模式加载示例代码段。
1
2
3
4
5
6
7
8
9
python复制代码pipeline = transformers.pipeline(
"text-generation",
model="meta-llama/Meta-Llama-3-8B-Instruct",
model_kwargs={
"torch_dtype": torch.float16,
"quantization_config": {"load_in_4bit": True},
"low_cpu_mem_usage": True,
},
)

LLama 3的未来

尽管当前的 8B(80亿)和 70B(700亿)参数模型给人留下了深刻印象,但 Meta 的工程师正在研究支持超过 400B(4000亿)参数的更大模型。这些模型仍在训练中。在未来几个月里,它们将更强的新功能,如多模态性、多语言对话能力、更长的上下文理解能力以及整体更强的能力。

Meta的LLama 3 最引人注目的一点就是开源。发布的基于文本的模型是LLama 3系列模型中的第一批。正如 Meta 所说,他们的主要目标是使 LLama 3 多语言和多模态,拥有更长的上下文支持,并持续改进核心大型语言模型(LLM)能力(如推理和编码)的整体性能。

我们迫不及待地想看看 GenAI 领域的下一个热点会是什么了。

最后,如果你正在计划训练自己的大语言模型,欢迎注册体验 DigitalOcean 旗下的 GPU 云服务 “Paperspace”,支持包括 H100、A100、4090 等多种 GPU,并预装 ML 框架。随时扩展,按需停止,只需按使用量付费。

如果需要预约更多 GPU 资源或希望了解方案详情,可以与 DigitalOcean 中国区独家战略合作伙伴卓普云咨询。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【高级RAG技巧】使用二阶段检索器平衡检索的效率和精度 一

发表于 2024-04-26

一 传统方法

之前的文章已经介绍过向量数据库在RAG(Retrieval Augmented Generative)中的应用,本文将会讨论另一个重要的工具-Embedding模型。
在这里插入图片描述一般来说,构建生产环境下的RAG系统是直接使用Embedding模型对用户输入的Query进行向量化表示,并且从已经构建好的向量数据库中检索出相关的段落用户大模型生成。但是这种方法很明显会受到Embedding模型性能的影响,比如是否支持多语言、跨语言检索、训练数据的质量等。因此,以改进Embedding模型为目标提升RAG系统性能一般会有两种做法:

方法1,在使用向量模型获取密集向量后,再使用Cross-encoder作为精排模型,对第一次召回的结果进行重排,以提高最终结果的质量。
方法2,使用稀疏向量搭配密集向量进行召回。密集向量即对整个文本进行向量化表示产生的向量,稀疏向量则是对原始文本的编码表示,如TF-IDF,BM25等。其中,稀疏向量可以帮助识别和捕捉特定的语义信息,比如文本中的关键词,而密集向量则可以提供更丰富的语义表达,是通过大量长文本学习而来的。通过同时将这两种向量进行召回,可以获得更丰富、更全面的信息,从而提升 RAG 的效果。

方法1和方法2既可以独立使用,也可以搭配使用,这就大大增加了算法工程师的武器库,通过搭积木的方式来提高RAG系统的效果。

二 Reranker模型剖析

本文主要讨论二阶段检索的方法,对于稀疏向量的讨论不在本文范围之内。

使用精排模型的原因

原因之一,向量数据库中存储大量的向量数据,而想要从大量数据中精确地检索出最相似的向量势必会带来极大的延迟,这在任何应用中都是无法接受的,因此向量数据库一般都会通过建立索引来优化查找过程,这实际上是一种近似检索而非精确检索,是准确与效率的折衷方案。如果数据库中没有那么多的向量,或者全表检索的时间损耗能够接受,则完全没必要用近似检索,但这在实际生产系统中是不可能的。

其二,LLM受限于上下文长度的限制,所以能利用到的向量片段数量是有限的,尽管现在越来越多的研究使得LLM的上下文窗口越来越长,如改进位置编码或者注意力机制等,但也有实验表明再长的上下文输入给大模型总会由遗失或者处理不好的情况,称为“Lost in middle”现象。因此,更好的办法是把更有用的上下文筛选出来,重点在于提升质而不是量。

精排模型结构

下面有两个模型,分别是Bi-Encoder和Cross-Encoder。左边的Bi-Encoder实际上就是我们常见的向量化模型,需要注意的是模型的结构并不是有2个Bert,而是只有1个Bert模型,但是对于两个句子分别做了向量化表示,最终比较这两个向量的相似度。这里‘Bi’表示的是处理的过程,而不是实际的结构。

在这里插入图片描述

而精排模型用到的Cross-Encoder结构则是把两个句子的输入concat到一起,最终预测这两个句子是否相关,有点类似于Bert训练时的next sentence prediction(NSP)。不难发现,这实际上就是一个分类模型,而且并不会产生具体的向量表示,只会产生一个介于0和1之间的值,用于表示句子对的相似性。而且,在使用时,需要将Query与待查询的句子拼接到一起再输入给模型,所以这就决定了输入的句子对数量不能太多,否则带来的计算耗时将会是无法估量的。

精排模型的本质

直观上来讲,用Bi-Encoder“目的更加通用”,是为了得到输入的向量化表示,这些向量不仅有大小,还有方向,因此就有这么一个学习向量化时遇到的经典例子:国王−男人=女王−女人国王-男人=女王-女人国王−男人=女王−女人。

而使用了Cross-Encoder的精排模型,因为输入部分包含了两个原始的句子,因此是从文本空间直接表示句子的相似度,而不是到了向量空间再表示。具体而言就是:传统的向量是经过了模型输出之后再根据不同的算子计算向量之间的相似度,而这个模型是句子对由模型得到端到端的结果。

能否再使用一次向量比较?或者说一阶段后再对检索出的向量计算精确的相似度有意义吗?
意义不大。因为这时候就算再计算精确的向量相似度,也只是在经过转换后的向量空间进行比较,因为这个空间比较“通用”,必定有所损失,无法在某种程度上准确表示所有的向量。

精排模型为什么准呢?
如前面说的,Bi-Encoder必须将所有的文档都映射到一个向量中,这个过程会丢失信息。而且,检索时query和已有的向量是没有交互的,是离线的状态,只是通过额外的相似度计算方法得到比较相似的向量,对query来说,缺乏必要的上下文信息。

而刚好精排模型是同时对两个句子进行编码的,这时候对query来说会含有上下文信息,而且从原始空间映射到隐空间是一个句子对而不是单独的句子,也更保证最终得到的相似度包含用户查询的文档信息。但是相对的,精排模型的结构决定了它的计算是实时的,而且计算量会比单纯的相似度计算大不少。

现有的精排模型

现有开源的Reranker模型已经非常多了,国内比较出门的有Jina-8k、bge-reranker以及bce-reranker等,且更新得非常快,目前看起来就是哪个最新用哪个,基本差异不大。值得一提的是Jina的模型是基于Alibi改进的注意力机制,所以能支持8K的上下文窗口,而bce本身只支持512k的上下文窗口,但是对较长的上下文进行切分,并且以其中一段最好的得分作为最终的相似度。bge模型没看过源码,等以后有时间针对这几个模型再研究一番。

总结

虽然目前二阶段方法用来提升RAG的性能表现越来越受到关注,但是具体来看,其中所含的技术都是早就有的内容。Cross-Encoder这种架构在当时显得比较鸡肋,只能用来比较句子的相似度,甚至无法输出向量,在大部分自然语言处理场景中都不受待见,谁能想到在如今又焕发生机了呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微软一面:订单超时未支付,如何自动关闭? 方案1 定时器

发表于 2024-04-26

hi,你好,我是猿java

最近,有小伙伴私信反馈微软一面的系统设计题:订单超时未支付,如何自动关闭?

说实话,微软能问出这种面试题确实很诧异,难道是互联网人已经大量涌进了微软这个养老基地,开始卷它?言归正传,有网购经验的小伙伴应该知道,如果订单在规定的时间内没有支付,订单就会被系统自动关闭,作为技术人员,该如何设计这个功能,今天我们来分析 4种有概括性的方案。

说明:

  • 规定的时间:即超时时间,这个因平台而异,有的场景是 10分钟超时,比如 12306 购票,有的平台是 15分钟或者 30分钟超时,有些秒杀场景是 1分钟超时,具体超时值需要根据具体的业务来定。
  • 自动关闭的原因:对于有库存的业务,需要释放库存,否则容易导致少卖。

方案1: 定时器

定时器应该是我们最可能想到的方案,通过定时器去数据库轮询待支付的订单信息,然后在业务代码中判断订单是否超时,更新订单状态。

实现思路

下单的时候把超时需要关闭订单的具体时间值(pay_expire_time)保存到表里,给pay_expire_time字段添加一个索引,然后设置一个定时器,每 3s/5s 去数据库查询一次数据,查询 SQL 如下:

1
sql复制代码select id from order where order_status = '待支付' && pay_expire_time > now()

优点

定时器方案最大的优点就是实现简单

缺点

  1. 需要定时从数据库拉取数据,对数据库会产生一定的压力
  2. 需要将数据加载到内存中进行业务逻辑处理,如果拉取的数据量比较大,可能会消耗过多的内存导致OOM
  3. 定时器是周期性的,对于超时订单的关闭可能存在延时

适用场景

适用于数据量比较小的业务场景,比如,很多小公司在业务处于 0-1 的过程中,该方案比较常见。

方案2: 被动关闭

服务器不做订单的主动关闭,而是在客户端拉取订单时按需被动关闭订单,比如,客户端通过接口获取服务器的时间以及订单超时时间,然后计算并判断订单是否超时,如果超时,将订单显示成超时的样式,
并隐式向服务器发送一个订单超时关闭的请求,服务器再做相应的逻辑处理。

优点

  • 实现简单
  • 按需处理订单超时,客户端需要用到数据再做判断

缺点

  • 订单长时间未关闭,在业务意义上可能是脏数据
  • 客户端需要做超时的逻辑判断并且发送超时关闭请求

适用场景

适用于数据量比较小的业务场景,被动关闭通常会结合定时器方案使用。

方案3: MQ延时消息

有些 MQ有延时消息的特性,比如阿里开源的 RocketMQ,用户创建订单时,同时往 MQ中发送一条延时消息,用户支付成功后,删除对应的延时消息(开源版本的没有此功能)
,当消费者消费到消息时再做超时的相关处理

优点

  • 利用中间件 MQ的特性,避免业务端自己实现

缺点

  • 开源的 RocketMQ 消息延时只有特定的几个级别,所以业务的超时时间要和 RocketMQ的延时级别相匹配才能使用
  • 强依赖中间件 MQ,所以 MQ的稳定性以及是否丢消息直接影响该功能
  • 使用开源版的 RocketMQ,没有删除延时消息的功能,因此需要对每条消息都做超时判断,增加了很多无效的数据处理
  • 如果数据量比较大,MQ可能会堆积产生延时

适用场景

适用于搭建了 RocketMQ的公司,想利用 RocketMQ的延时消息来实现该功能

方案4: 超时中心

很多大厂都有自己的分布式超时系统 TOC(Timeout Coordinator System),不同的公司可能叫法不一样,这里给出一种实现思路:

实现思路

1. 订单创建

  • 用户通过前端下单,请求发送到OMS(Order Management System)订单管理系统
  • OMS创建订单,并将订单状态设置为“待支付”或类似的初始状态

2. 注册超时事件

  • 在订单创建的同时,OMS向 TOC注册一个超时事件
  • 超时事件包含了订单ID、超时时间(例如30分钟)和超时后需要执行的动作(例如关闭订单)

3. 超时监控

  • TOC系统开始监控这个订单的超时事件
  • 如果在设定的超时时间内,用户完成了支付,OMS会更新订单状态并通知 TOC取消超时事件

4. 超时处理

  • 如果超时时间到达,而订单状态仍然是“待支付”,TOC将触发超时动作
  • TOC通知 OMS订单已超时

5. 订单关闭

  • OMS收到 OTC超时通知后,执行订单关闭操作
  • OMS将订单状态更新为“已关闭”或“超时关闭”

6. 超时后处理

  • OMS可能需要处理一些后续任务,如释放库存、退还优惠券等
  • 完成这些操作后,该订单的超时处理流程结束

优点

  • 专门的人做专门的事,性能,稳定性,扩展性有保障
  • 可以处理海量数据

缺点

  • 实现和维护成本比较高
  • 实现复杂度比较高,方案中可能会涉及分布式调度,MQ等技术栈

适用场景

适合数据量比较大的中大型公司,需要专门的团队负责

如何选择?

架构领域有句经典名言:没有最好的架构,只有够用(合适)的架构。

这个理论用于订单超时关闭场景同样适用,分布式超时中心方案看起来高大上,而且能处理海量数据,但是,如果把这套方案用于几个人的团队,有一种杀鸡用牛刀的架势,可能不但解决不了问题,还会给团队带来技术难题和成本负担。

因此,架构一定要基于业务,方案也一定要基于业务。

对于定时器方法,看起来简单粗暴,但是很多公司的业务在 0-1的过程中,它就是首选,因为实现的成本很低,没有什么技术难点,
假设每条订单的数据大小是 1K,5万条订单的数据大概在 50M左右,用定时器把数据加载到内存中处理也是ok的,所以该方案适合每天的订单量在万级别,或者再保守一些,每日订单总量在 1万以下。

对于 MQ延时消息方案,首先需要考虑自建 MQ 还是购买 MQ云产品,以及两种方案带来的技术难点或者购买成本,基于先有 MQ这个条件再看它适合的业务场景,在根据每日的订单量是百万级别,十万级别还是万级别来选择 MQ的配置。

分布式超时中心方案技术难度和维护成本都比较高,一般适合一些独角兽企业,大厂或者财力和技术能力都不错的公司。

总结

本文分析了具有概括性的 4种订单超时关闭方案,也是业内使用比较多的方案,可能每个公司实现的细节或者叫法略有差异,但是思想是相通的。

当然还有其他方案实现,比如 Redis,但是不推荐

通过本文,我们可以根据不同的业务体量给出不同的方案或者设计思路,以及各个方案的优缺点,具体细节可以结合具体业务细节。

没有最好的架构,只有够用(适合)的架构,很多时候在选择方案的时候一定要结合实际业务,切勿过度设计,但是也不能缺乏设计的能力

原创好文

  • 字节 2面:MySQL枚举类型该如何定义? enum?tinyint?char?varchar?
  • 腾讯女后端设计了一套短链系统,当场就想给她offer!
  • 肝了一周,彻底弄懂了 CMS收集器原理,这个轮子造的真值!
  • 9款常见的 JVM垃圾回收器
  • 美团一面:Git 是如何工作的?(推荐阅读)
  • 阿里 P7二面:Redis 执行 Lua,能保证原子性吗?
  • 当下环境,程序员需要修炼的 3项技能
  • AI是打工人的下一个就业风口吗?
  • 和斯坦福博士写代码的一个月
  • 肝了一周,这下彻底把 MySQL的锁搞懂了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《斋藤康毅-深度学习入门》读书笔记04-神经网络的学习 深度

发表于 2024-04-26

I’m a very very quick learner. – Lou Bloom, Nightcrawler

本章主题是神经网络如何自主地从数据中学习,从而得到最优权重和偏置参数值。由此引入了损失函数的概念,用来量化当前的权重参数的不合适度,学习的目标就是找到让损失函数最小的那一组参数值,使用的方法是梯度法。

深度学习的意义

由于实际的神经网络具有海量的层数和参数,参数可达成千上万乃至上亿个,凭人力根本无法进行计算,因此需要深度学习来确定参数。

对于机器学习来说,在识别手写数字的场景里,通常需要先从图像中提取特征量(表现为向量的形式),然后通过机器学习技术来学习这些特征量的模式。特征量用来把图片转换为向量。

而对于深度学习而言,提取特征量这一操作也是由机器学习完成的。

image.png
图:人工学习->机器学习->深度学习

过拟合 over fitting

指参数对某一个数据集过度拟合的情况,即对该数据集精确度很高,但对其它数据集则不然。

过拟合是深度学习中经常要面对的一个问题。

损失函数 loss function

神经网络以损失函数作为量化指标,来寻找最优权重参数,一般采用均方误差和交叉熵误差。

均方误差 mean squared error

image.png
图:均方误差算式

各参数说明如下:

  • yk:神经网络输出
  • tk:监督数据
  • k:数据维度
1
2
3
py复制代码# 均方误差
def mean_squared_error(y, t):
return 0.5 * np.sum((y-t)**2)

one-hot表示

将正确解标签表示为1,其他标签表示为0。如在数字识别的例子里,一条监督数据是[0,0,1,0,0,0,0,0,0,0],表示当前数字为2。如果使用非one-hot表示,则t直接就是2。

交叉熵误差 cross entropy error

image.png
图:交叉熵误差的算式

解标签tk采用one-hot表示,只有正确解为1,其它值为0。因此上式可以简化为E=-logyk。自然对数log是在(0,1]区间单调递增的,递增范围是负无穷大~0,因此yk越接近1,E的值越小,也就表明越接近正确(最佳)结果。

1
2
3
4
py复制代码# 交叉熵误差
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))

作为保护性对策,增加一个极小值delta防止出现log(0)的边缘场景。

mini-batch 学习

为了在一次运算中使用更多数据验证参数的损失值,可以通过mini-batch的思路,即每次对批量数据计算损失函数,先求和再平均,已进行正规化。

image.png
图:批量计算交叉熵误差

叫mini-batch是因为相对于完整的数据集,只选取其中的一小部分(mini)进行计算。防止计算量过大。可以理解为抽样调查

1
2
3
4
5
6
py复制代码# 通过numpy进行抽样
train_size = x_train_.shape[0] # 60000条
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size) # 随机抽取10个下标
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

这段逻辑用如果用Java也能实现,但肯定比不上python简洁

numpy中关于array的维度操作

1
2
3
4
5
6
7
8
9
10
11
py复制代码# nparray_test.py 测试
import numpy as np

a = np.array([[11,22,33], [44,55,66]])
print(a.ndim) # 2
print(a.size) # 6,即数组内元素个数
b = a.reshape(1, a.size)
print(b) # [[11 22 33 44 55 66]]
print(b.ndim) # 2,仍然是二维
print(b.size) # 6,元素数不变
print(b.shape) # (1,6)

数值微分

导数用来表示某个瞬间的变化量,即瞬时速度。

image.png
图:导数

  • 前向差分:f(x+h)与f(x)之间的差分,因为偏离所以有误差
  • 中心差分:f(x+h)与f(x-h)之间的差分,更接近真实值
1
2
3
4
py复制代码# 中心差分
def numerical_diff(f, x)
h = 1e-4 # 0.0001
return (f(x+h)-f(x-h))/(2*h)

数值微分例子

对于下述二次函数,通过python代码计算其微分,以及在函数上x=5处的斜率,并绘图。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
py复制代码import numpy as np
import matplotlib.pylab as plt

def numerical_diff(f, x):
h = 1e-4 # 0.0001
return (f(x+h) - f(x-h)) / (2*h)

def function_1(x):
return 0.01*x**2 + 0.1*x

def tangent_line(f, x):
d = numerical_diff(f, x)
print(d)
y = f(x) - d*x
return lambda t: d*t + y

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)

image.png

偏导数

含有多个变量的函数的导数称为偏导数,在使用时应当声明是对其中哪一个变量求导。

image.png

上式的python实现为:

1
2
3
py复制代码def function_2(x):
return x[0]**2 + x[1]**2
# 或 return np.sum(x**2)

对应图像是:

image.png

其中对x0、x1求导分别写作:

image.png

在求导时,将已知参数值带入,对未知参数进行中心差分求导,例如求x0=3、x1=4时关于x0的偏导数:

1
2
3
4
py复制代码def function_tmp1(x0):
return x0**2 + 4.0**2.0

numerical_diff(function_tmp1, 3.0) # 6.00000

梯度

导数代表某个时间的瞬时速度,对于多维向量每一维求导,则得到了它的梯度(gradient)。当我们声明梯度时,需要说明是在x0=?、x1=?、...xn=?所有变量处的梯度。

image.png

对于函数f和多维参数x计算梯度的方法如下(含批处理):

谨记:x是一个多维向量(即张量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
py复制代码import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x)

for idx in range(x.size):
tmp_val = x[idx]
x[idx] = float(tmp_val) + h
fxh1 = f(x) # f(x+h)

x[idx] = tmp_val - h
fxh2 = f(x) # f(x-h)
grad[idx] = (fxh1 - fxh2) / (2*h)

x[idx] = tmp_val # 还原值

return grad


def numerical_gradient(f, X):
if X.ndim == 1:
return _numerical_gradient_no_batch(f, X)
else:
grad = np.zeros_like(X)

for idx, x in enumerate(X):
grad[idx] = _numerical_gradient_no_batch(f, x)

return grad

梯度指向各点处函数值减小最快的方向。

image.png

梯度法

梯度法就是利用梯度的概念来寻找函数最小值的方法,通过不断沿着梯度方向前进,进而不断缩小损失函数值。

  • 极小值:局部最小值
  • 最小值:全局最小值
  • 鞍点:从某个方向看是极小值,从另一方向看是极大值

寻找最小值的梯度法称为梯度下降法(gradient descent method),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。一般来说神经网络中梯度法是指梯度下降法。

在某一点上根据梯度方向前进,这就是梯度法的大白话表述。前进的步长用η表示,称为学习率(learning rate)。学习率决定在一次学习(前进)中,应当学习多少,以及在多大程度上更新参数。像学习率这样影响深度学习的值,称为超参数,以与权重等普通参数区分。

image.png
图:更新(学习)一次

学习率过大或者过小,都无法抵达一个“好的位置”,在神经网络中一般会一边改变学习率的值一半尝试学习,以便确认学习是否正常进行。

  • 学习率过大:得到一个很大的发散的值
  • 学习率过小:没学完就结束了

梯度下降法的python实现:

1
2
3
4
5
6
7
8
py复制代码def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x

for i in range(step_num): # 前进步数
grad = numerical_gradient(f, x) # 该点梯度
x -= lr * grad # 向梯度方向前进

return x

神经网络的梯度

在使用神经网络时,需要得出最优的权重矩阵,利用梯度法可以对这个矩阵进行计算,权重W神经网络和梯度表示如下:

image.png

由上例,定义一个simpleNet的简单神经网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
py复制代码import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 预定一个随机参数神经网络

def predict(self, x): # 计算网络输出
return np.dot(x, self.W)

def loss(self, x, t): # 计算交叉熵损失值
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)

return loss

x = np.array([0.6, 0.9]) // 输入
t = np.array([0, 0, 1]) // 标签集

net = simpleNet()

f = lambda w: net.loss(x, t) # 使用lambda简化函数写法
dW = numerical_gradient(f, net.W)

print(dW) # 打印神经网络的梯度

有了上面的梯度算法,就可以设置步长和步数,经过迭代得到最小损失函数。

学习算法实现

调整权重和偏置以便拟合训练数据的过程称为学习,分为以下步骤:(由于第一步是随机抽样,因此该方法也称为随机梯度下降法 stochastic gradient descent)

  1. 抽样 mini-batch:从训练数据中随机选出一部分数据,用这部分数据进行学习
  2. 计算梯度:随机设定权重,计算各个权重参数梯度,梯度表示损失函数的值减小最多的方向
  3. 更新参数:由上一步得出的梯度计算新的权重参数
  4. 重复1~3步

类定义:双层神经网络 TwoLayerNet

首先定义双层网络的结构,它包含每一层的权重和偏置,并且提供计算输出的predict函数,计算交叉熵损失的loss函数,计算批量精确度的accuracy函数,以及生成梯度矩阵的numerical_gradient函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
py复制代码# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

# hidden_size 隐藏层(第1层)的神经元数
# weight_init_std 初始权重
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 维度为input_size * hidden_size
self.params['b1'] = np.zeros(hidden_size) # 偏置初始化为0
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) # hidden_size * output_size
self.params['b2'] = np.zeros(output_size)

# 根据定义计算输出
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']

a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1) # 隐藏层激活用sigmoid
a2 = np.dot(z1, W2) + b2
y = softmax(a2) # 输出层激活用softmax

return y

# x:输入数据, t:监督数据
def loss(self, x, t):
y = self.predict(x)

return cross_entropy_error(y, t)

# 这里x可以是batch输入
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)

accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy

# x:输入数据, t:监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t) # 损失

grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

return grads # 损失的梯度

# 计算梯度
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {} # 初始化为Map

batch_num = x.shape[0]

# forward 前向输出
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)

# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)

da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)

return grads

实现深度学习

引入epoch的概念,它是一个单位,表示训练集中全部数据均被使用过一次时的更新次数。对于10000条数据的训练集来说,如果每个mini-batch学习100条,则epoch=100。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
py复制代码# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10) #

iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1 # 步长

train_loss_list = [] # 列表记录损失下降
train_acc_list = [] # 列表记录精度上升
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
# 取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

# 计算梯度
#grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch) # 每次学习都更新一遍梯度

# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]

loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)

if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc : {:.4f}, test acc : {:.4f}".format(train_acc, test_acc))

# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

在每个epoch里,计算一次训练集和测试集的精度accuracy,并且显示在图像上,两条曲线吻合,说明没有发生过拟合。

image.png
图:未发生过拟合

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…111213…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%