TensorFlow从零手写ResNet18:80行代码详解残差连接与BasicBlock实现

1. 为什么80行代码值得认真拆解

ResNet18这个网络结构,搞深度学习的人几乎没有不知道的。2015年那篇残差学习的论文出来之后,基本上把深层网络训练难的问题给解决了。但很多人第一次接触ResNet,要么是直接调tf.keras.applications.ResNet50,要么是看论文里的公式推导,真正自己从零手写一遍ResNet18的人反而不多。

我当初也是这么过来的。最开始用预训练模型做迁移学习,几行代码就能跑起来,感觉ResNet不过如此。后来有一次需要改网络结构,把残差块里的某些层替换掉,才发现如果对底层实现不熟悉,连改都不知道从哪下手。从那以后我就养成了一个习惯:任何一个经典网络,都要自己手写一遍。

这篇文章要聊的就是用TensorFlow从零实现ResNet18,整个核心代码控制在80行左右。你可能会问,80行够吗?够的。ResNet18本身结构并不复杂,它的精髓在于残差连接的设计思想,而不是堆砌多少代码。这80行里包含了BasicBlock的定义、残差连接的实现、网络主体的搭建、以及完整的编译和训练流程。

适合谁看?如果你已经了解卷积神经网络的基本概念,知道卷积层、批归一化、激活函数大概是干什么的,但还没有自己动手搭过一个完整的ResNet,那这篇内容就是写给你的。如果你已经用过ResNet做项目,但想搞清楚Keras的Functional API和Model子类化两种写法有什么区别,这里也会涉及。

我选择TensorFlow而不是PyTorch来写,原因很简单:Keras的Functional API在搭建这种有跳连结构的网络时,表达非常直观。你用layers.add()把两个分支加起来的那一刻,残差连接的概念就变得非常具象了。PyTorch当然也能写,但TensorFlow的Keras接口对新手更友好一些,尤其是调试阶段,model.summary()一出来,整个网络结构一目了然。

下面我会从设计思路开始,把每一层的选择理由讲清楚,然后给出完整的代码实现,最后分享一些我在实际训练中踩过的坑和排查技巧。代码你可以直接复制去跑,但更重要的是理解每一行为什么这么写。

2. ResNet18的整体设计思路拆解

2.1 残差学习的核心逻辑:为什么需要跳连

要理解ResNet18,得先理解残差连接到底解决了什么问题。在ResNet之前,大家普遍认为网络越深,表达能力越强,效果应该越好。但实际训练中发现,当网络深度增加到一定程度,准确率反而下降了。这不是过拟合,因为训练集上的误差也在变大。这个问题被称为退化问题。

退化问题的根源在于,深层网络在反向传播时,梯度经过多层传递后容易消失或者爆炸,导致前面的层很难得到有效的更新。残差连接的做法很巧妙:它不让网络直接学习目标映射H(x),而是学习残差映射F(x) = H(x) - x。然后通过跳连把输入x直接加到输出上,最终输出变成F(x) + x。

这样做的好处是,如果某一层什么都没学到,F(x)趋近于0,输出就等于输入x,相当于恒等映射。这意味着即使多加了几层没有用的网络,最差情况也不会比原来更差。而在实际训练中,网络总能学到一些有用的残差特征,所以效果只会更好。

用生活化的类比来说,这就像你搬家的时候,不是把所有东西重新打包一遍,而是在原有打包好的箱子上,只调整需要改动的部分。大部分箱子直接搬过去就行,省时省力。

2.2 BasicBlock的结构设计:两层卷积加一条捷径

ResNet18使用的是BasicBlock,这是ResNet系列里最基础的残差块。一个BasicBlock包含两个3x3卷积层,每个卷积后面跟一个批归一化层和一个ReLU激活函数。输入通过跳连直接加到第二个卷积的输出上,然后再经过ReLU激活。

具体结构是这样的:输入x先经过第一个3x3卷积,步长可能是1也可能是2,取决于这个块是不是下采样块。然后经过BN和ReLU。接着经过第二个3x3卷积和BN。这时候得到F(x),再把原始的x加过来。但这里有个细节:如果输入和输出的通道数不一样,或者空间尺寸因为步长变化了,就不能直接相加。这时候需要在跳连上做一个1x1卷积来调整维度,保证能加在一起。

ResNet18的整体架构是这样的:首先是一个7x7的大卷积,步长2,输出64个通道,后面跟BN和ReLU,再跟一个3x3的最大池化,步长2。然后依次经过四个阶段,每个阶段包含两个BasicBlock。第一个阶段通道数是64,第二个阶段128,第三个256,第四个512。每个阶段的第一个BasicBlock可能会做下采样,把空间尺寸减半、通道数翻倍。最后是一个全局平均池化层和一个全连接层输出分类结果。

2.3 为什么选择TensorFlow的Functional API

TensorFlow提供了三种搭建模型的方式:Sequential、Functional API和Model子类化。Sequential适合线性堆叠的网络,但ResNet有跳连,不是线性的,所以Sequential直接排除。Model子类化最灵活,你可以写任意复杂的逻辑,但调试起来麻烦一些,因为网络结构是在call方法里动态定义的,summary()有时候看不清楚。

Functional API是折中方案。它用层作为函数,接收张量返回张量,你可以像搭积木一样把层拼起来。跳连操作就是简单的layers.add([x, shortcut]),非常直观。而且Functional API构建的模型天然支持summary()、plot_model()这些工具,调试方便。

我个人的习惯是:如果网络结构是静态的、可以用计算图表示的,优先用Functional API。只有需要动态控制流(比如根据输入长度决定循环次数)的时候,才用Model子类化。ResNet18显然是静态结构,所以Functional API是最优选择。

3. 核心细节解析与实操要点

3.1 卷积层参数选择的门道

写ResNet18的时候,卷积层的参数设置有几个关键点需要注意。第一个是卷积核大小。BasicBlock里用的是3x3卷积,这是经过大量实验验证的黄金尺寸。两个3x3卷积堆叠的感受野等于一个5x5卷积,但参数量更少,非线性表达能力更强。第一个7x7卷积是ResNet论文里的设计,用来快速降低空间分辨率,同时提取底层特征。

第二个是步长的设置。7x7卷积步长为2,后面的最大池化步长也为2,这样输入图像经过这两层之后,空间尺寸直接变成原来的四分之一。以224x224的输入为例,经过7x7卷积变成112x112,再经过池化变成56x56。然后四个阶段分别输出56x56、28x28、14x14、7x7的特征图。

第三个是padding策略。3x3卷积用padding='same',这样当步长为1时,输入输出空间尺寸不变。当步长为2时,TensorFlow的same padding会自动计算需要的填充量,保证输出尺寸是输入的一半。这个细节很重要,如果你用padding='valid',尺寸计算会变得很麻烦,容易出错。

第四个是偏置项。因为卷积层后面紧跟着批归一化层,BN层会减去均值,所以卷积的偏置项其实没有作用,反而浪费参数。所以我在代码里统一设置use_bias=False。这是一个小细节,但能体现你对网络结构的理解深度。

3.2 批归一化的位置与作用

批归一化在ResNet里是标配,每个卷积层后面、激活函数前面都会加一个BN层。BN的作用是对每个mini-batch的数据做归一化,让均值接近0、方差接近1,然后通过两个可学习参数gamma和beta来恢复表达能力。

为什么BN对ResNet这么重要?因为残差连接会把输入直接加到输出上,如果两个分支的数值尺度差异很大,相加之后会导致数值不稳定。BN把每个分支的输出都规范化到相似的尺度,相加的时候就不会出现某个分支主导的情况。

在代码实现上,BN层有一个axis参数需要注意。对于TensorFlow的channels_last格式(默认格式),特征图的形状是(batch, height, width, channels),所以axis=3或者axis=-1。如果你用的是channels_first格式,那就要改成axis=1。这个参数设错了,BN就完全失效了,而且不会报错,只是训练效果变差,非常隐蔽。

3.3 跳连的两种处理方式

跳连是ResNet的灵魂,但实现的时候要分两种情况处理。第一种情况是输入输出维度一致,直接相加就行。第二种情况是维度不一致,需要用一个1x1卷积加BN来调整输入的通道数和空间尺寸,然后再相加。

判断维度是否一致的标准有两个:通道数是否相同,空间尺寸是否相同。在ResNet18里,每个阶段的第一个BasicBlock会做下采样,步长为2,同时通道数翻倍。这时候跳连就需要用1x1卷积,步长也设为2,把输入映射到相同的维度。

我在代码里用一个use_projection标志来控制是否启用投影捷径。当步长不为1或者输入通道数不等于输出通道数时,就需要投影。这个逻辑封装在BasicBlock的call方法里,写一次就够了。

注意:投影捷径里的1x1卷积后面也要跟BN,但不需要ReLU激活。因为跳连的目的是传递原始信息,加非线性激活反而会破坏信息的完整性。

3.4 全局平均池化的优势

ResNet18的最后部分,传统做法是先展平特征图,然后接一个全连接层。但ResNet论文里用的是全局平均池化,直接把每个通道的特征图取平均值,得到一个长度为通道数的向量,然后再接全连接层。

全局平均池化的好处有两个。第一,大幅减少参数量。以512个通道的7x7特征图为例,如果展平后接全连接层,参数量是51277类别数,非常大。用全局平均池化之后,参数量只有512类别数。第二,全局平均池化有正则化效果,因为每个通道只输出一个值,相当于强制网络学习每个通道的全局特征,而不是依赖某些局部位置的信息。

在实际写代码的时候,layers.GlobalAveragePooling2D()一行就够了,它会自动处理空间维度。然后接一个layers.Dense(num_classes, activation='softmax')输出分类概率。

4. 完整代码实现与逐段解析

4.1 BasicBlock的代码实现

先来看BasicBlock的实现。我把它写成一个自定义的Keras层,继承自tf.keras.layers.Layer。这样做的好处是结构清晰,可以像普通层一样调用。

import tensorflow as tf from tensorflow.keras import layers, Model class BasicBlock(layers.Layer): def __init__(self, filters, stride=1): super().__init__() self.conv1 = layers.Conv2D(filters, 3, strides=stride, padding='same', use_bias=False) self.bn1 = layers.BatchNormalization() self.conv2 = layers.Conv2D(filters, 3, strides=1, padding='same', use_bias=False) self.bn2 = layers.BatchNormalization() self.relu = layers.ReLU() self.use_projection = stride != 1 if self.use_projection: self.shortcut_conv = layers.Conv2D(filters, 1, strides=stride, use_bias=False) self.shortcut_bn = layers.BatchNormalization() def call(self, inputs, training=None): x = self.conv1(inputs) x = self.bn1(x, training=training) x = self.relu(x) x = self.conv2(x) x = self.bn2(x, training=training) if self.use_projection: shortcut = self.shortcut_conv(inputs) shortcut = self.shortcut_bn(shortcut, training=training) else: shortcut = inputs x = layers.add([x, shortcut]) return self.relu(x)

这段代码有几个细节值得展开说。第一,training参数贯穿整个call方法,传递给BN层。这是因为BN在训练和推理时的行为不同:训练时用当前batch的均值和方差,推理时用移动平均的均值和方差。如果不传这个参数,BN层可能无法正确切换模式。

第二,use_projection的判断条件是stride != 1。在ResNet18里,通道数变化和步长变化是同步的,所以只需要判断步长就够了。但如果你要改网络结构,比如通道数变了但步长没变,那判断条件就要改成stride != 1 or inputs.shape[-1] != filters。

第三,跳连的加法用layers.add([x, shortcut]),这是Functional API的写法。如果你用Model子类化,也可以直接写x + shortcut,效果一样。但用layers.add的好处是它会自动处理广播和类型转换,更稳妥。

4.2 网络主体的搭建

有了BasicBlock,搭建ResNet18就很简单了。我用Functional API来写,整个网络定义大概30行。

def build_resnet18(num_classes=1000): inputs = layers.Input(shape=(224, 224, 3)) x = layers.Conv2D(64, 7, strides=2, padding='same', use_bias=False)(inputs) x = layers.BatchNormalization()(x) x = layers.ReLU()(x) x = layers.MaxPooling2D(3, strides=2, padding='same')(x) for filters, stride, blocks in [(64, 1, 2), (128, 2, 2), (256, 2, 2), (512, 2, 2)]: for i in range(blocks): x = BasicBlock(filters, stride if i == 0 else 1)(x) x = layers.GlobalAveragePooling2D()(x) outputs = layers.Dense(num_classes, activation='softmax')(x) return Model(inputs, outputs)

这段代码里,我用了一个循环来构建四个阶段。每个阶段的配置是(通道数, 第一个块的步长, 块的数量)。ResNet18每个阶段都是2个块,所以blocks固定为2。第一个块的步长决定了是否下采样,后面的块步长都是1。

这里有一个容易踩坑的地方:BasicBlock的实例化。每次循环都创建了一个新的BasicBlock对象,这是正确的。如果你不小心把同一个BasicBlock对象重复使用,Keras会报错,因为一个层对象只能被调用一次(除非你显式地允许重用)。所以一定要在循环内部创建新实例。

另外,layers.Input的shape参数不包含batch维度。如果你要处理不同尺寸的输入,可以把shape设为(None, None, 3),这样网络就能接受任意尺寸的图像。但要注意,全局平均池化层会自动适应空间尺寸,所以全连接层的输入维度不会受影响。

4.3 模型编译与训练配置

模型搭好之后,编译和训练的代码就很标准了。我一般用Adam优化器,学习率设0.001,损失函数用交叉熵,评估指标用准确率。

model = build_resnet18(num_classes=10) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) model.summary()

model.summary()会打印出每一层的名称、输出形状和参数量。ResNet18的参数量大约在1100万左右,如果你看到这个数字,说明网络结构是对的。如果参数量差很多,那就要检查是不是某个阶段的通道数设错了,或者BasicBlock的数量不对。

训练的时候,如果你用的是小数据集(比如CIFAR-10),输入尺寸可以改成32x32。但要注意,原来的7x7卷积步长2加最大池化步长2,会把32x32直接降到8x8,后面的下采样阶段可能空间尺寸太小。这时候可以把第一个7x7卷积改成3x3,去掉最大池化,或者调整步长。这是一个常见的适配问题,很多人直接拿ResNet18跑CIFAR-10,发现效果不好,就是因为输入尺寸和网络结构不匹配。

4.4 参数量计算与验证

ResNet18的参数量可以手动算一下,验证代码实现是否正确。第一个7x7卷积:77364 = 9408。每个BasicBlock里,两个3x3卷积的参数量分别是336464和3364*64,加起来是73728。四个阶段的通道数分别是64、128、256、512,每个阶段2个块,参数量逐阶段翻倍。

具体算下来,卷积层参数量大约是1100万,全连接层参数量是512*类别数。如果类别数是1000,全连接层就是51.2万。总参数量约1150万,和Keras打印出来的数字对得上。

如果你发现参数量对不上,最常见的原因是BN层的参数没算进去。每个BN层有2个可学习参数(gamma和beta),还有2个不可学习的移动均值和方差。虽然移动均值和方差不算在可训练参数里,但它们会占用内存。model.summary()里会分别显示可训练参数和不可训练参数,你可以对照检查。

5. 常见问题与排查技巧实录

5.1 训练不收敛的排查思路

ResNet18训练不收敛,最常见的原因有几个。第一个是学习率太大。Adam的默认学习率是0.001,对ResNet18来说通常没问题,但如果你改了batch size或者数据集,可能需要调整。我一般会先用0.001跑几个epoch,看loss是不是在下降。如果loss震荡很厉害,就降到0.0001试试。

第二个原因是BN层没有正确设置training参数。如果你在自定义层里忘了把training传给BN,BN在训练时可能用的是推理模式,均值和方差不会更新,导致训练效果很差。这个问题的隐蔽性很强,因为代码不会报错,只是效果不好。排查方法是打印BN层的移动均值和方差,看它们有没有在训练过程中变化。

第三个原因是数据预处理不对。ResNet论文里对输入图像做了标准化,减去ImageNet的均值,除以标准差。如果你直接输入0-255的原始像素值,网络很难训练。正确的做法是把像素值缩放到0-1之间,或者做标准化。我一般用tf.keras.applications.resnet.preprocess_input,它会自动做正确的预处理。

5.2 显存不足的优化策略

ResNet18虽然不算大,但在显存有限的显卡上训练,还是可能遇到OOM(Out of Memory)的问题。最直接的解决方法是减小batch size。batch size从32降到16或者8,显存占用会明显下降。但batch size太小会影响BN的效果,因为BN依赖batch内的统计量。如果batch size小于8,可以考虑用Group Normalization替代BN。

另一个方法是使用混合精度训练。TensorFlow支持mixed_float16策略,把大部分计算用float16来做,显存占用可以减少将近一半,速度也能提升。用法很简单,在构建模型之前加几行代码就行。

policy = tf.keras.mixed_precision.Policy('mixed_float16') tf.keras.mixed_precision.set_global_policy(policy)

但要注意,混合精度训练时,最后的输出层要强制用float32,否则softmax可能会数值不稳定。可以在Dense层里加dtype='float32'参数。

还有一个方法是梯度累积。如果你只能用很小的batch size,但又想模拟大batch的效果,可以累积几个小batch的梯度再更新一次参数。TensorFlow里可以用tf.GradientTape手动实现,但代码会复杂一些。Keras的fit方法不直接支持梯度累积,需要自己写训练循环。

5.3 迁移学习中的层冻结技巧

用ResNet18做迁移学习的时候,通常会把前面的卷积层冻结,只训练最后的全连接层。冻结的方法是把层的trainable属性设为False。

base_model = build_resnet18(num_classes=1000) for layer in base_model.layers[:-3]: layer.trainable = False

但这里有一个坑:如果你在冻结之前已经编译了模型,冻结操作不会生效。必须先设置trainable,再编译模型。这是因为Keras在编译时会记录哪些参数需要更新,之后再改trainable不会影响已经编译好的优化器。

另外,BN层在冻结时有个特殊处理。即使你把BN层的trainable设为False,它在训练时仍然会更新移动均值和方差。如果你希望BN层完全冻结,需要显式地设置layer.trainable = False并且确保training=False。在Keras里,当整个模型被冻结时,BN层会自动切换到推理模式,但在微调部分层的时候,这个行为可能不符合预期。

我一般的做法是:先冻结所有卷积层,只训练全连接层几个epoch,让分类头先适应新数据。然后解冻最后几个卷积块,用更小的学习率做微调。这样比一上来就全部解冻效果更好,也更稳定。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
loss不下降学习率过大打印每步loss降低学习率到0.0001
loss震荡batch size太小检查batch size增大batch size或改用GroupNorm
准确率上不去数据预处理不对检查输入像素范围做标准化处理
显存OOMbatch size过大查看显存占用减小batch size或混合精度
BN层不更新training参数未传递打印BN移动均值在call方法中传递training
参数量对不上通道数配置错误对照论文结构检查每个阶段的filters
训练速度慢数据管道瓶颈查看GPU利用率用tf.data优化输入管道

6. 实操心得与扩展建议

6.1 我踩过的三个坑

第一个坑是BN层的axis参数。有一次我写了一个自定义的卷积块,BN层的axis设成了1,但我的数据格式是channels_last,结果BN完全失效,训练了50个epoch准确率还在10%左右。排查了半天才发现是axis的问题。从那以后,我养成了一个习惯:每次写BN层,都显式地写axis=-1,并且打印一下输入形状确认。

第二个坑是跳连的加法顺序。layers.add([x, shortcut])和layers.add([shortcut, x])在数学上是等价的,但在TensorFlow里,如果两个张量的形状不完全一样,广播规则可能会导致意外的结果。我建议始终保持一致的顺序,把主分支放在前面,跳连分支放在后面,这样出问题的时候容易定位。

第三个坑是模型保存和加载。用Functional API构建的模型,保存成H5格式没问题。但如果你用了自定义层(比如我写的BasicBlock),加载的时候需要提供custom_objects参数,否则Keras不认识这个层。更稳妥的做法是保存成SavedModel格式,它会自动包含自定义层的定义。

model.save('resnet18_model') loaded_model = tf.keras.models.load_model('resnet18_model')

6.2 从ResNet18扩展到其他变体

理解了ResNet18的实现,扩展到ResNet34就很简单了。ResNet34的BasicBlock结构和ResNet18一样,只是每个阶段的块数量从2变成了[3, 4, 6, 3]。你只需要改一下循环里的blocks配置就行。

如果要扩展到ResNet50,就需要换Bottleneck结构了。Bottleneck用三个卷积:1x1降维、3x3卷积、1x1升维。这样做的目的是在保持表达能力的同时减少参数量。ResNet50的参数量比ResNet34多不了太多,但深度增加了不少。

再进一步,你可以把ResNet18改成SE-ResNet,在每个BasicBlock里加一个Squeeze-and-Excitation模块。SE模块的作用是自适应地调整每个通道的权重,让网络关注更重要的特征。实现起来也不复杂,加一个全局平均池化、两个全连接层和一个Sigmoid激活就行。

6.3 80行代码的边界与取舍

最后说一下这80行代码的边界。它实现的是ResNet18的核心结构,可以直接用于训练和推理。但它没有包含数据加载、数据增强、学习率调度、模型检查点保存这些工程化的内容。这些内容如果都加进来,代码量会翻好几倍。

我的建议是:先把核心结构跑通,确认网络能正常前向传播和反向传播,然后再逐步添加工程化的组件。不要一上来就追求大而全的代码,那样反而容易迷失在细节里。理解每一行代码的作用,比复制粘贴一百行代码更有价值。

如果你想让这80行代码更实用,我建议优先加两个东西:一是数据增强,用tf.keras.layers.RandomFlip和RandomRotation做在线增强,能显著提升小数据集上的效果;二是学习率衰减,用tf.keras.callbacks.ReduceLROnPlateau在验证集loss不下降时自动降低学习率,能帮你省去手动调参的麻烦。

这两个东西加进去,代码大概会多出20行,但训练效果和稳定性会有明显提升。至于更复杂的分布式训练、混合精度、梯度累积,等你真正需要的时候再加也不迟。

相关新闻

Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

Hyper-Laplacian先验快速去卷积:查找表加速非凸优化

简介:这份资源是图像去模糊领域经典论文《Fast Image Deconvolution using Hyper-Laplacian Priors》的配套Matlab实现,面向从事图像恢复、计算摄影与视觉研究的学生和开发者,用于解决模糊图像复原及盲去卷积中卷积核与清晰图像联合估计的问题…

2026/10/20 13:38:46 阅读更多 →
10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

10月arXiv机器学习论文盘点:强化学习、长尾分布与数据效率

1. 另类入口:当arXiv变成一座每周更新的森林如果说学术搜索像一张精准的地图,那arXiv的cs.LG板块就更像一片每周定时更新的森林——你永远知道自己能在这里找到最新的机器学习论文,但也正因为"最新"这两个字,每次走进来…

2026/10/20 13:38:46 阅读更多 →
AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

AI编程工具进化:从代码补全到能独立交付项目的“许愿机”

说实话,我已经不太想用“Coding Agent”来称呼手上这批AI开发工具了。不是矫情,是最近连续几周用它们做真正的项目——不是教它写个冒泡排序或者生成一段样板代码,而是从需求梳理、数据结构设计到部署脚本、线上监控告警的完整链路走下来——…

2026/10/20 13:37:45 阅读更多 →

最新新闻

大模型API同日降价后,开发者如何重新选型与优化成本

大模型API同日降价后,开发者如何重新选型与优化成本

1. 这场"同日降价"到底发生了什么先把时间线捋清楚。某天早上,两家头部模型厂商几乎在同一时间窗口宣布了旗下旗舰模型的价格调整:一边是 Claude Opus 5.5,另一边是 GPT-6 的 Sol 和 Luna 两个版本。注意,这不是那种&qu…

2026/10/20 14:20:28 阅读更多 →
DeepSeek职场提效指南:V3与R1引擎选型、提示词写法与智能体搭建

DeepSeek职场提效指南:V3与R1引擎选型、提示词写法与智能体搭建

简介:面向职场效率提升与 AI 工具落地的一站式 DeepSeek 应用指南,适合企业员工、创意工作者、研究人员及技术爱好者,也适合希望系统学习提示语技巧的新手。内容从 DeepSeek 基础模式切入,系统对比 V3 基础模型与 R1 深度思考模型…

2026/10/20 14:20:28 阅读更多 →
AI本地化部署值不值?从数据、延迟与成本看企业决策

AI本地化部署值不值?从数据、延迟与成本看企业决策

过去一年,我身边问他"到底要不要把AI部署到本地"的企业朋友越来越多。这个问题听起来简单,背后却藏着好几层焦虑:有的担心核心数据被外部平台看到,有的嫌按调用次数付费太贵,还有的纯粹是看到同行上了私有化…

2026/10/20 14:20:28 阅读更多 →
PCIe 4.0底层机制深度解析:从128b/130b编码到信号完整性工程实践

PCIe 4.0底层机制深度解析:从128b/130b编码到信号完整性工程实践

简介:PCIE 4.0 Base 1.0 规范是PCI-SIG于2017年8月28日发布的第四代PCI Express总线官方正式版本,面向硬件工程师、驱动开发者和系统架构师,用于理解PC/服务器/存储及外设互连中的高速串行传输设计;16 GT/s速率、140ns延迟、低功耗…

2026/10/20 14:20:28 阅读更多 →
2025企微私域AI功能横评:五大维度实测哪家真能用

2025企微私域AI功能横评:五大维度实测哪家真能用

这几个月我一直在倒腾企业微信私域工具,起因其实挺简单:2025年开年以来,各家企微服务商都把AI功能当成了主卖点,什么智能话术生成、会话质检、客户意向打分、朋友圈文案一键出稿,朋友圈里几乎天天刷到类似宣传。但广告…

2026/10/20 14:20:28 阅读更多 →
科研场景下大模型技能包配置指南:论文写作、数据可视化与汇报材料三大核心技能包

科研场景下大模型技能包配置指南:论文写作、数据可视化与汇报材料三大核心技能包

1. 科研场景下大模型技能包的真实需求拆解科研工作流和普通的内容创作、代码开发有本质区别。一篇论文从选题到最终发表,中间要经历文献调研、实验设计、数据处理、图表绘制、论文撰写、同行评审回复、会议汇报等多个环节,每个环节对AI辅助的需求都不一样…

2026/10/20 14:19:19 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →