【智能优化算法】GA-遗传算法基本原理

算法背景

Genetic Algorithm (GA):遗传算法,属于进化计算技术(evolutionary computation),1967年,美国密歇根大学 J. Holland 教授的学生 Bagley 在他的博士论文中首次提出了遗传算法这一术语,但早期研究缺乏带有指导性的理论和计算工具的开拓。1975年, J. Holland 等提出了对遗传算法理论研究极为重要的模式理论并出版了相关书籍,20世纪80年代后,遗传算法广泛应用于多个领域。

该算法借鉴了达尔文的进化论和孟德尔的遗传学说,基于"适者生存"的原则,通过模拟自然选择中的繁殖、交叉、变异等操作和对相关个体的选择,从而促进个体和群体的不断迭代进化,最终得到目标搜索空间内的近似最优解。

基本思想和原理

基本思想

基本思想 :优胜劣汰,适者生存

生物进化与自然选择:存在一片草原,该草原上生活着一群羊,每当遭遇生存困难时,羊群中老弱病残的个体, 即不适应环境的个体更容易被淘汰,而其中健壮的个体,即适应环境的个体更容易生存下来,未被淘汰的个体的后代通常会继承它们的优良性状,该种群会面临着 淘汰、繁殖、淘汰、繁殖... 的循环过程,从整个种群来看,这群羊会朝着整体更适合在这片草原上生存的方向发展。

上述场景的抽象化:为了便于介绍该算法如何基于"优胜劣汰,适者生存"的思想来解决数学问题,这里将上述场景进行抽象化处理。

① 种群/个体抽象化:种群是一组个体,而个体则由能反应它适应环境程度的特征来表示

  • 个体:每只羊都是一个个体。由于对于每只羊,它们在草原上可以拥有不同的适应程度,这个适应程度和不同个体的某些特征相关,因此用一组反映该个体特征的数字来表示,这组数字又被称为染色体(位串),我们基于某种编码方法将某个解中对应的各变量取值编码后组成染色体,该染色体就是个体的表现形式。
  • 种群:每只羊都是一个个体,那一群羊就是种群。种群为一组个体的集合,即一组染色体的集合。

② 求解空间抽象化:草原为种群生活的环境,种群中的每个个体对该环境的适应程度各不相同,而我们期望得到最适合环境的个体,这个过程无非就是从求解空间中搜索环境的最优解

  • 求解空间:每个个体的各特征的变化(染色体的变化)与它对环境的适应度相关,在遗传算法中,我们认为染色体的变化是有限的,每个个体就是一个解,个体(染色体)能产生的所有可能的变化,就是求解空间.
  • 适应度:个体对环境的适应程度不同,这种适应程度被称为适应度
  • 适应度函数:个体对环境的适应程度和该个体所具有的特征相关,适应度函数输入染色体,输出该个体对应的适应度.

③ 生物进化抽象化:个体拥有进化的能力,且进化的方向一般朝着更适应环境的方向进行

  • 进化 :染色体是个体的表现形式,通过适应度函数可将位串映射为对应的环境适应度,我们可以通过制定某些策略使染色体发生变化,这些变化就赋予了个体进化的能力.
    • 交叉:两染色体之间可能进行交叉操作,按某种交叉策略互换其中的部分值
    • 变异:单个染色体进行变异操作,染色体的各个值可能按某种变异逻辑发生改变
  • 选择:在迭代过程中,我们应当对种群中的个体进行选择,使适应度高的个体 "生存" 下来,而适应度低的个体被淘汰.

思考一下,上述过程无非就是用 环境 这一标准对某个生物种群进行不断地筛选,最后得到对环境适应程度强的个体,其实就是在个体所有可能产生的变化中搜索环境的最优解 ,而最终产生的 "适应度强的个体" 就是环境的最优解,即:"适应度最强的个体" 就是对 "环境" 的最优解搜索结果。

为了使算法更易于理解,本文抽象出以上概念,一些名词在其它博客或书籍中可能有着不同的叫法,不过概念基本都是类似的,本文就不对此深究了。

重要概念解释

在具体介绍GA具体逻辑之前,先对该算法中涉及的一些重要参数或概念进行介绍以便于后续原理的讲解。

设在 d d d 维求解空间中存在 n n n 个染色体,则有:

  • i i i 个染色体: X i = ( x 1 , x 2 , ⋯   , x d ) X_i = (x_1, x_2, \cdots, x_d) Xi=(x1,x2,⋯,xd)
  • 编码后的第 i i i 个染色体: X i ′ = ( x 1 ′ , x 2 ′ , ⋯   , x d ′ ) X_i' = (x_1', x_2', \cdots, x_d') Xi′=(x1′,x2′,⋯,xd′)
  • 所有个体的适应度: F = ( f 1 , f 2 , ⋯   , f d ) F = (f_1, f_2, \cdots, f_d) F=(f1,f2,⋯,fd)
  • 各个个体对应被选择的概率: P = ( p 1 , p 2 , ⋯   , p d ) P = (p_1, p_2, \cdots, p_d) P=(p1,p2,⋯,pd)
  • 交叉率,变异率: p c r o s s , p m u t p_{cross}, p_{mut} pcross,pmut

不难理解,在 d d d 维空间中,每个染色体都有编码前的值,编码后的值,以及对应的适应度和在本轮迭代中被选择(保留)下来的概率,本文认为适应度越大越好,也就是目标函数(适应度函数)的值越大越好,若目标函数为越小越好,则通常取其负数。

算法逻辑与原理

算法逻辑

遗传算法的总体逻辑:

  1. 根据预先设置的各变量的范围在求解空间中随机初始化各个染色体,每个染色体用 X i X_i Xi 来表示
  2. 将各个染色体的值 X i X_i Xi 编码得到 X i ′ X_i' Xi′
  3. 对于每次迭代:
    1. 交叉:从种群中随机抽取两个染色体,执行交叉操作。该过程循环执行 n ⋅ p c r o s s n \cdot p_{cross} n⋅pcross 次.
    2. 变异:根据预先定义的变异率 p m u t p_{mut} pmut,使用轮盘赌算法得到各个染色体是否发生变异,对发生变异的染色体执行变异操作。
    3. 选择:先计算所有个体的适应度 F F F,根据 F F F 来求得各个个体对应被选择的概率 P P P,然后使用轮盘赌算法基于 P P P 对当前种群进行 n n n 次随机抽样,得到 n n n 个染色体组成的新种群。这一过程中适应度越大的染色体对应概率越高,也就越容易被抽到,反之适应度越低的染色体对应概率越小,越不容易被抽到。
  4. 直到算法收敛或达到某种条件,停止迭代

算法得总体逻辑如上方所示,其中的核心逻辑为编解码和每次迭代中的交叉、变异、选择。对于编解码,编码操作将染色体中的各个值转换为更利于计算机处理的形式,而解码操作则为编码的逆过程,即将计算机处理的形式转换回原来的各个值。交叉、变异赋予了个体进化的能力,变异在一定程度上赋予了种群跳出局部最优解的能力,而选择操作则用来保证整个种群进化的方向是朝着适应度更高的方向进行的,在这三个过程中迭代,种群中各个个体的适应度会越来越高。一般到达预先指定的迭代次数后,停止迭代,将此时种群中适应度最高的染色体作为搜索结果。

编码与解码

不同的编码方式适用的应用场景也不同,本文在这里主要介绍在一般情况适用较多的二进制编码。

二进制编码:将染色体中的各个值转化为更易于处理的二进制形式。编码的目的是将指定区间内的十进制数按一定精度转换为对应从0开始的正整数区间(新区间)内的二进制表示,共三个步骤,即计算二进制位数,线性映射和二进制转换。

二进制解码:执行编码操作的逆过程即可,即先将各维度的数值从二进制转换为十进制,然后从新区间映射到原区间即可。

二进制也可以表示正负数、浮点数,但这样会使得问题更加复杂,为了保证简单,因此转换为从0开始的正整数区间。

① 计算二进制位数:原区间的大小和精度的不同,表达它们所需的二进制位数也不同
2 L − 1 ≥ ( u p − l o w ) ⋅ 1 0 t ⟶ L ≥ l o g 2 ( u p − l o w ) ⋅ 1 0 t + 1 \begin{align} 2^L - 1 \ge (up - low) \cdot 10^t \quad \longrightarrow \quad L \ge log_2(up-low)\\cdot 10\^t + 1 \end{align} 2L−1≥(up−low)⋅10t⟶L≥log2(up−low)⋅10t+1

  • u p up up:某变量取值范围的上界
  • l o w low low:某变量取值范围的下界
  • t t t:精度,即保留小数的位数
  • L L L:二进制位数,取满足条件的最小值

为什么是 1 0 t 10^t 10t :例如在原区间 0 , 2 0, 2 0,2 中,若精度为1,我们可以将该区间看作 0.0 , 2.0 0.0, 2.0 0.0,2.0 需要表示21个数就可以了,转换为对应从0开始的正整数区间为 0 , 20 0, 20 0,20 而若精度为2,则可以看作 0.00 , 2.00 0.00, 2.00 0.00,2.00 则要表示201个数,转换为对应从0开始的正整数区间为 0 , 200 0, 200 0,200,其中 0 对应原区间的 0.00,1 对应原区间的 0.01,2 对应原区间的 0.02,...,199 对应原区间的 1.99,200 对应原区间的 2.00。

为什么是 2 L − 1 2^L-1 2L−1 :从上方解释中也可看出,表示的数的个数中每次都要多1个数,因此这里要减1。也可以从另一个角度理解,例如在原区间 0 , 1 0,1 0,1 以精度为2来表示时,对应从0开始的正整数区间为 0 , 200 0, 200 0,200,二进制表示的最大值为 200,设二进制位数为 L L L,该二进制数所能表达的最大数为 2 L − 1 2^L - 1 2L−1,因此需要保证 2 L − 1 ≥ 200 2^L - 1 \ge 200 2L−1≥200。

:某变量的取值范围为 − 1 , 1 -1, 1 −1,1,保留2位小数,求能表达该范围取值的二进制位数.
易得有 : 2 L − 1 ≥ 2 ⋅ 1 0 2 ⟶ L ≥ l o g 2 201 取满足条件的最小值,即 : L = 8 \begin{align} &易得有:\space 2^L - 1 \ge 2 \cdot 10^2 \space \longrightarrow \space \space L \ge log_2 201 \\ &取满足条件的最小值,即: L = 8 \end{align} 易得有: 2L−1≥2⋅102 ⟶ L≥log2201取满足条件的最小值,即:L=8

② 线性映射:得到新区间后,需要将原区间的各个值线性映射到新区间中,得到在新区间中的对应值
y = ( x − l o w ) ⋅ 2 L − 1 u p − l o w y = (x - low) \cdot \frac{2^L-1}{up - low} y=(x−low)⋅up−low2L−1

  • x x x:原区间的值
  • y y y:新区间的对应值
  • L L L:二进制位数
  • u p up up:原区间的上界
  • l o w low low:原区间的下界

对于任何区间的线性映射 :假设原区间 l o w 1 , u p 1 low_1, up_1 low1,up1 ,新区间为 l o w 2 , u p 2 low_2, up_2 low2,up2,有如下变换:
l o w 1 , u p 1 ⟶ 减去原区间上界 0 , u p 1 − l o w 1 ⟶ 除以原区间极差 0 , 1 ⟶ 乘以新区间极差 0 , u p 2 − l o w 2 ⟶ 加上新区间下界 l o w 2 , u p 2 low_1, up_1 \stackrel{减去原区间上界}{\longrightarrow} 0, up_1-low_1 \stackrel{除以原区间极差}{\longrightarrow} 0, 1 \stackrel{乘以新区间极差}{\longrightarrow} 0, up_2 - low_2 \stackrel{加上新区间下界}{\longrightarrow} low_2, up_2 low1,up1⟶减去原区间上界0,up1−low1⟶除以原区间极差0,1⟶乘以新区间极差0,up2−low2⟶加上新区间下界low2,up2

将上述变换整理成数学公式则为:
y = ( x − l o w 1 ) ⋅ u p 2 − l o w 2 u p 1 − l o w 1 + l o w 2 y = (x - low_1) \cdot \frac{up_2 - low_2}{up_1 - low_1} + low_2 y=(x−low1)⋅up1−low1up2−low2+low2

  • x x x:原区间的值
  • y y y:新区间的对应值
  • l o w 1 , u p 1 low_1, up_1 low1,up1:原区间的下界和上界
  • l o w 2 , u p 2 low_2, up_2 low2,up2:新区间的下界和上界

其实就是把原区间平移、放缩,变换到了新区间

:原区间 − 10 , 15 -10, 15 −10,15 存在值 3,它在区间 5 , 10 5, 10 5,10 中的对应值就是 3 − ( − 10 ) ⋅ 10 − 5 15 − ( − 10 ) + 5 = 7.6 3-(-10) \cdot \frac{10 - 5}{15-(-10)} + 5 = 7.6 3−(−10)⋅15−(−10)10−5+5=7.6.

二进制编码中的线性映射更加简单,因为新区间的下界已经确定(下界为0),在 ① 中得到二进制位数后也已知上界,将原来染色体中的各个值线性映射到新区间即可,需要注意的是,映射到新区间的数必须为整数,若非整数,通常采用四舍五入转换为整数。

:原区间 − 1 , 1 -1,1 −1,1 存在值 0.5,精度为2。

易得二进制位数为 8,则新区间为 0 , 2 8 − 1 0, 2\^8 - 1 0,28−1,将 0.5 映射到新区间的值为 0.5 − ( − 1 ) ⋅ 2 8 − 1 1 − ( − 1 ) = 191.25 = 191 0.5 - (-1) \cdot \frac{2^8 - 1}{1 - (-1)} = 191.25 = 191 0.5−(−1)⋅1−(−1)28−1=191.25=191,即线性映射的结果为 191.

③ 二进制转换:线性映射后得到正整数,将该正整数转为二进制进行表示即可。

二进制编码案例 :求函数 f ( x 0 , x 1 ) = x 0 + x 1 , x 0 ∈ − 1 , 1 , x 1 ∈ 2 , 5 f(x_0, x_1) = x_0 + x_1, x_0 \in -1, 1, x_1 \in 2, 5 f(x0,x1)=x0+x1,x0∈−1,1,x1∈2,5 的最大值问题中,不难发现维度为 2,若设置精度为2,假设当前某染色体为 0.5 , 3 0.5, 3 0.5,3,求该染色体编码后的结果。

  1. 计算二进制位数:可以看出维度为2,分别计算两个维度下的二进制位数,易得 L 0 = 8 , L 1 = 9 L_0=8, L_1=9 L0=8,L1=9.

{ L 0 ≥ l o g 2 ( ( 1 + 1 ) ⋅ 1 0 2 + 1 ) L 1 ≥ l o g 2 ( ( 5 − 2 ) ⋅ 1 0 2 + 1 ) ⟶ { L 0 ≥ l o g 2 201 L 1 ≥ l o g 2 301 \begin{cases} L_0 \ge log_2((1+1) \cdot 10^2 + 1) \\ L_1 \ge log_2((5-2) \cdot 10^2 + 1) \\ \end{cases} \quad \longrightarrow \quad \begin{cases} L_0 \ge log_2 201 \\ L_1 \ge log_2 301 \\ \end{cases} {L0≥log2((1+1)⋅102+1)L1≥log2((5−2)⋅102+1)⟶{L0≥log2201L1≥log2301

  1. 线性映射:易得两维度的新区间分别为 0 , 2 8 − 1 , 0 , 2 9 − 1 0, 2\^8-1, 0, 2\^9-1 0,28−1,0,29−1,将 0.5 , 3 0.5, 3 0.5,3 映射后得到 191 , 170 191, 170 191,170.

0.5 − ( − 1 ) ⋅ 2 8 − 1 1 − ( − 1 ) = 191.25 = 191 ( 3 − 2 ) ⋅ 2 9 − 1 5 − 2 = 170.33 = 170 \begin{align} 0.5 - (-1) \cdot \frac{2^8 - 1}{1 - (-1)} = 191.25 = 191 &\\ (3 - 2) \cdot \frac{2^9 - 1}{5-2} = 170.33 = 170 & \end{align} 0.5−(−1)⋅1−(−1)28−1=191.25=191(3−2)⋅5−229−1=170.33=170

  1. 二进制转换:将 191 , 170 191, 170 191,170 分别转换为8位和9位二进制数得到 10111111 , 010101010 10111111, 010101010 10111111,010101010.

编码后的染色体主要用于执行交叉、变异等策略,而解码后的染色体则用于求解适应度,以及我们最后得到的结果中各变量的对应值。

轮盘赌算法

以案例来讲解,例如通过轮盘来抽奖,其中抽中一等奖、二等奖、三等奖、没有奖的概率分别为 0.1, 0.2, 0.3, 0.4,如下图所示:

拨动转盘,抽到没有奖的概率最大,抽到一等奖的概率最小。虽然一等奖的概率最小,但也是有可能抽到的,以一定概率进行随机抽样,这就是轮盘赌算法。

选择

针对不同场景,选择部分也有着不同的策略,这里主要介绍基于轮盘赌算法的选择策略。

假设种群大小为 n n n ,保证种群大小不变,适应值越大的染色体被保留的概率越大,若已知每个染色体被保留的概率,基于轮盘赌算法按指定概率重复进行 n n n 次抽样即可得到新群体。

对于每个染色体被保留的概率,最简单的想法就是将每个染色体的适应值分别除以所有染色体适应值的总和,但这种思路无法适应度为负数的情况,因此采用以下计算方式:

已知所有个体的适应度 F = ( f 1 , f 2 , ⋯   , f n ) F=(f_1, f_2, \cdots, f_n) F=(f1,f2,⋯,fn),将其中每个个体的适应度都取其绝对值,得到 ∣ F ∣ = ( ∣ f 1 ∣ , ∣ f 2 ∣ , ⋯   , ∣ f n ∣ ) |F| = (|f_1|, |f_2|, \cdots, |f_n|) ∣F∣=(∣f1∣,∣f2∣,⋯,∣fn∣),记 F F F 中最小的数为 f m i n f_{min} fmin, ∣ F ∣ |F| ∣F∣ 中最小的数为 ∣ f ∣ m i n |f|{min} ∣f∣min,则有如下变换:
f i ′ = f i − f m i n + ∣ f ∣ m i n f_i' = f_i - f
{min} + |f|_{min} fi′=fi−fmin+∣f∣min

F F F 中的每个元素应用以上变换,得到 F ′ F' F′,然后再计算对应概率:
p i = f i ′ s u m ( F ′ ) p_i = \frac{f_i'}{sum(F')} pi=sum(F′)fi′

这样就得到了每个染色体对应被选中的概率,然后基于轮盘赌算法抽样 n n n 次即可。

直接让每个元素减去最小值会出现 0,经过计算会出现被保留概率为0的染色体,而我们希望尽管适应度低,但它仍然存在被保留的可能,这时候我们可以再让整体平移,即都增加一个数,但随着增加的数的大小变化,求得的概率也会产生相应变化,即增加的数字越大,得到的概率极差就越小。由于当所有染色体的适应度都是正数的时候,这时候不应该进行平移,因此我们将增加的数值统一设置为所有染色体适应值的绝对值的最小值,可能对于存在负数时并不是一个很好的策略,但也未必很糟糕,效果上没太大出入。

例1 :已知某3条染色体的适应值分别为 5 , 10 , 15 5, 10, 15 5,10,15,变换后得到 5 , 10 , 15 5, 10, 15 5,10,15,对应的概率为 0.167 , 0.333 , 0.5 0.167, 0.333, 0.5 0.167,0.333,0.5

例2 :已知某3条染色体的适应值分别为 − 5 , 5 , 10 -5, 5, 10 −5,5,10,变换后得到 5 , 15 , 20 5, 15, 20 5,15,20,对应的概率为 0.125 , 0.375 , 0.5 0.125, 0.375, 0.5 0.125,0.375,0.5

交叉

交叉也有着很多策略,本文主要介绍单点交叉。

交叉操作发生在两个不同染色体的同一维度下。

与生物学上的染色体交叉类似,在遗传算法中也就是互换数值中的一部分,这种变换可以改变染色体的数值,进而改变它的适应度,因此交叉操作赋予了个体进化的能力。

单点交叉 :以一个案例来介绍,假设维度为2,二进制位数分别为 8,9 的两个染色体分别为 c h r o m 1 : 11001011 , 010010101 , c h r o m 2 : 01111010 , 111100100 chrom_1:11001011, 010010101, chrom_2: 01111010, 111100100 chrom1:11001011,010010101,chrom2:01111010,111100100,首先在两个维度分别从 0 , 8 , 0 , 9 0,8, 0,9 0,8,0,9 选取一个随机数,以 3 , 6 3, 6 3,6 为例,将维度1下两染色体的值的索引3之后的各bit位互换,将维度2下两染色体的值的索引6之后的各bit位互换,如下图所示:

执行交叉操作的两条染色体从当前种群中随机抽取。假设种群大小为 n n n,交叉率为 p c r o s s p_{cross} pcross,将"随机从种群中抽取两条染色体进行交叉操作"执行 n ⋅ p c r o s s n \cdot p_{cross} n⋅pcross 次即可。

变异

变异也有着很多策略,本文主要介绍基本位变异。

在二进制编码后,基本位变异的策略也非常简单易懂,即随机改变染色体某一维度或多个维度下的一位或多位,若该位为0,则变为1,若为1,则变为0。这里以改变所有维度的一位为例,假设染色体 c h r o m : 11001011 , 010010101 chrom: 11001011, 010010101 chrom:11001011,010010101 发生变异,则先在两个维度分别从 0 , 8 , 0 , 9 0,8, 0,9 0,8,0,9 选取一个随机数,以 2 , 8 2, 8 2,8 为例,将该染色体维度1的数值中索引为2的地方改变数值,维度2的数值中索引为8的地方改变数值即可,如下图所示:

变异率通常较低,因此通常基于轮盘赌法来决定各个染色体是否发生变异,即让种群中的每个染色体都 "拨动一次轮盘",若发生变异,则执行对应逻辑即可。

变异赋予了种群中个体跳出局部最优解的能力,试想一下,当几乎整个种群都位于局部最优解时,也许有那么一两个个体通过变异跳转到了其它更好的位置,由此带领整个种群跳出局部最优解。

相关技巧和优化思路

自适应交叉率与变异率:交叉率赋予了个体进化的能力,与选择结合,交叉率越大,算法的开发能力越强,但种群更可能陷入局部最优解。个体发生变异有助于跳出局部最优解,变异率越大,算法的探索能力越强,但种群可能跳出全局最优解。为了避免陷入最优解,在迭代初期,变异率应该大,而在迭代后期,整个种群已经趋于收敛到算法所认为的最优解范围了,这时变异率应该较小,避免某些个体在发现最优解后经过变异错失最优解。一个简单的思路是设置变异率最大值和最小值,然后随着迭代次数的增加,从最大值到最小值线性衰减。交叉率与之同理,也可以设置一个随着迭代次数的增加线性地由小变大的交叉率。

交叉率与变异率无法真正做到 "自适应",但我们可以在一定程度或一定范围上让它们按某种方式进行变化,以得到更好的算法性能。

总结

GA的逻辑稍显繁琐,其核心原理在于编解码、交叉、变异、选择,其中的一些细节也是必不可少的,在理解每个过程以及它们的作用后,回过头来看,其实逻辑也并不复杂,可能只是在一开始有些"难以接受",但理解原理后也会发现一切都"理所应当",无非就是在重复执行几个步骤而已。随着算法的发展,GA也衍生出各种变体和改进方案,上方介绍的遗传算法基本原理大体是考虑求解一般函数的最大值(最小值)这样的任务来介绍的,但实际上存在着各种各样的问题,我们要学习的是GA的思想,然后基于这种思想和原理来根据不同的问题制定特定的解决方案。

基于Python实现GA(GitHub)Algorithm/IOA/GA at main · Rink1t/Algorithm (github.com)

Reference

相关推荐
意图共鸣20 分钟前
意图共鸣科技《认知智能白皮书》——感知与执行分离:认知架构(CA)如何重塑大模型底层结构
人工智能·架构
等一个人的@22 分钟前
让数据自己开口:数睿通智库新增智能问数模块
人工智能·自然语言处理
ZGi.ai23 分钟前
人工审查节点:让自动化工作流多一步人工把关
运维·人工智能·自动化·人机协同·智能体工作流·人工审查
风吹夏回1 小时前
Python 全局异常处理:从“满屏 try-except”到优雅兜底
开发语言·python
王莎莎-MinerU1 小时前
MinerU 深度技术解析:从架构原理到生产部署的全面指南
css·人工智能·自然语言处理·架构·ocr·个人开发
盘古信息IMS1 小时前
盘古信息IMS V6 8.0重磅发布:以薪火AI数智平台点燃离散制造数智化引擎
大数据·人工智能·制造
kkeeper~1 小时前
0基础C语言积跬步之数据在内存中的存储
c语言·数据结构·算法
weilaieqi11 小时前
从音响制造到AI家庭娱乐生态:不见不散AI智能K歌音响亮相第二十届深圳国际金融博览会
人工智能·制造·娱乐
小熊Coding1 小时前
Python爬取当当网二手图书项目实战!
开发语言·爬虫·python·beautifulsoup·requests·二手图书
企服AI产品测评局1 小时前
Agent适配信创环境实测:企业级自动化如何实现国产操作系统与数据库全兼容?
运维·数据库·人工智能·ai·chatgpt·自动化