Bohrium
robot
新建

空间站广场

论文
Notebooks
比赛
课程
Apps
我的主页
我的Notebooks
我的论文库
我的足迹

我的工作空间

任务
节点
文件
数据集
镜像
项目
数据库
公开
AI+电芯 | 多任务预测电池容量和功率衰减
AI+电芯
AI+电芯
JiaweiMiao
发布于 2023-12-11
推荐镜像 :Basic Image:bohrium-notebook:2023-03-26
推荐机型 :c12_m92_1 * NVIDIA V100
4
ISEA_Cap_IR(v1)

AI+电芯 | 多任务预测电池容量和功率衰减

背景

锂离子电池会因使用和暴露于环境条件而退化,从而影响其存储能量和供电的能力。 由于固有的制造差异和耦合的非线性老化机制,准确预测锂离子电池的容量和功率衰减具有挑战性。 在notebook中,构建了一种数据驱动的预测框架,用于通过多任务学习同时预测容量和功率衰减。 该模型能够在生命早期阶段准确预测容量和内阻的退化轨迹以及拐点和寿命终止点。 验证显示,容量衰减和电阻上升预测的平均百分比误差分别为 2.37% 和 1.24%。 该模型能够准确预测退化,面对容量和电阻估计误差,进一步证明了该模型的鲁棒性和通用性。与预测容量和功率衰减的单任务学习模型相比,该模型显示出显着的预测精度提高和计算成本降低。 这项工作展示了多任务学习在锂离子电池退化预测中的亮点。

数据集

本工作中使用的电池退化数据集是从 48 个 Sanyo/Panasonic 18650 NMC/石墨 LIB 的循环老化测试中获得的。这些电池的标称容量为1.85 Ah,截止电压为3 V和4.1 V,并在25°C的相同老化场景下进行了测试。在循环测试中,电池在 3.5 V 至 3.9 V 之间以 4 A 恒流恒压 (CC-CV) 方式充电/放电 30 分钟。电荷周转率约为 1 Ah,对应于大约 20% - 80% SOC之间的循环。每个电池平均进行17次表征测试,每两次表征测试之间进行约160个充放电循环,其中表征测试进行不同电流倍率下的容量检查和混合脉冲功率表征测试。在这项工作中,**分别使用 90% SOC 附近 1 A 放电电流时的容量和 2 A 充电脉冲时的 2 s 电阻作为 SOH-C(容量衰减) 和 SOH-R(内阻增长) 的指标。**电池组的功率衰减通常由电池组的阻抗决定。

本Notebook搬运自battery degradation trajectory prediction research work at ISEA, RWTH Aachen University 以及 github

代码
文本
[14]
%%capture
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model
from tensorflow.keras.regularizers import *
from tensorflow.keras import Input
from tensorflow.keras.optimizers import Adadelta,Adam
from tensorflow.keras import regularizers
import tensorflow.keras as keras
import tensorflow.keras.backend as kb
import matplotlib.pyplot as plt
import pickle
代码
文本

定义模型参数。包括输入和输出的序列长度。

代码
文本
[3]
#define model parameters
tf.keras.backend.set_epsilon(1e-9)
cIntInputSeqLen=384
cIntOutputSeqLen=128
cIntProcFeatures=1
cIntHiddenNode=64
cIntMaskValue=0
代码
文本

自定义损失函数

代码
文本
[4]
#define custom loss function
class CustomLoss:
@staticmethod
def RMSE(y_true, y_pred):
return kb.sqrt(tf.keras.losses.mean_squared_error(y_true, y_pred))
@staticmethod
def MaskedRMSE(y_true,y_pred):
isMask = kb.equal(y_true, 0)
isMask = kb.all(isMask, axis=-1)
isMask = kb.cast(isMask, dtype=kb.floatx())
isMask = 1 - isMask
isMask = kb.reshape(isMask,tf.shape(y_true))
masked_squared_error = kb.square(isMask * (y_true - y_pred))
masked_mse = kb.sum(masked_squared_error, axis=-1) / (kb.sum(isMask, axis=-1)+kb.epsilon())
return kb.sqrt(masked_mse)
@staticmethod
def MaskedMSE(y_true,y_pred):
isMask = kb.equal(y_true, 0)
isMask = kb.all(isMask, axis=-1)
isMask = kb.cast(isMask, dtype=kb.floatx())
isMask = 1 - isMask
isMask = kb.reshape(isMask,tf.shape(y_true))
masked_squared_error = kb.square(isMask * (y_true - y_pred))
masked_mse = kb.sum(masked_squared_error, axis=-1) / (kb.sum(isMask, axis=-1)+kb.epsilon())
return masked_mse
@staticmethod
def MaskedMAE(y_true,y_pred):
isMask = kb.equal(y_true, 0)
isMask = kb.all(isMask, axis=-1)
isMask = kb.cast(isMask, dtype=kb.floatx())
isMask = 1 - isMask
isMask = kb.reshape(isMask,tf.shape(y_true))
masked_AE = kb.abs(isMask * (y_true - y_pred))
masked_mae = kb.sum(masked_AE, axis=-1)/ (kb.sum(isMask, axis=-1)+kb.epsilon())
return masked_mae
#numpy function wrapper
@staticmethod
@tf.function
def MaskedMAPE(y_true,y_pred):
return tf.py_function(CustomLoss.numpyMaskedMAPE ,(y_true, y_pred), tf.double)
@staticmethod
def numpyMaskedMAPE(y_true,y_pred):
MapeLst=list()
for elm_t,elm_p in zip(y_true,y_pred):
y_t=elm_t[0:np.count_nonzero(elm_t),:]
y_p=elm_p[0:np.count_nonzero(elm_t),:]
MapeLst.append(np.mean(((np.abs(y_t - y_p)+1e-10) / y_t)) * 100)
return np.array(MapeLst,dtype=np.float)
代码
文本

模型搭建

在模型输入时,为了保证输入输出的格式一致,会对输入和输出的数据进行填充使其达到长度一致。同时,为了使模型能认知到有些部分实际上是填充,应该忽略,需要增加Masking的步骤。在分别处理好容量数据和内阻数据后,模型会将两者拼接到一起,再进行下一步的处理。

代码
文本
[5]
#model structure definition
#input part
InputCap=tf.keras.layers.Input(shape=(cIntInputSeqLen,cIntProcFeatures))
MaskedInputCap=tf.keras.layers.Masking(mask_value=cIntMaskValue)(InputCap)
InputIR=keras.layers.Input(shape=(cIntInputSeqLen,cIntProcFeatures))
MaskedInputIR=tf.keras.layers.Masking(mask_value=cIntMaskValue)(InputIR)
CombInput = keras.layers.Concatenate(axis=-1)([MaskedInputCap, MaskedInputIR])
代码
文本

Encoder的部分,由多层LSTM组成。由于是一个sequence to sequence的预测问题,这里我们在encoder和decoder的部分的主体都使用了LSTM作为主要的结构。

代码
文本
[6]
#encoder part
EncCtext=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(CombInput)
EncCtext=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(EncCtext)
EncCtext=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(EncCtext)
EncCtextOut=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=False))(EncCtext)
CombCtext=tf.keras.layers.RepeatVector(cIntOutputSeqLen)(EncCtextOut)
代码
文本

在Decoder的部分,同样是由多层LSTM组成。作为多任务预测的模型,这里对容量和内阻分别构建了Decoder的部分,两者相互独立。模型的输出将预测出的容量衰减序列和内阻增长序列组合。

代码
文本
[7]
#decoder part for capacity
Dec1=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(CombCtext)
Dec1=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec1)
Dec1=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec1)
Dec1=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec1)
Dec1=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(cIntHiddenNode*2, activation="relu",kernel_regularizer=regularizers.l1_l2(l1=1e-5, l2=1e-4)))(Dec1)
Dec1=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(cIntHiddenNode/2, activation="relu"))(Dec1)
DecOutCap=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(1, activation="relu"))(Dec1)
代码
文本
[8]
#decoder part for IR
Dec2=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(CombCtext)
Dec2=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec2)
Dec2=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec2)
Dec2=tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(cIntHiddenNode,return_sequences=True))(Dec2)
Dec2=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(cIntHiddenNode*2, activation="relu",kernel_regularizer=regularizers.l1_l2(l1=1e-5, l2=1e-4)))(Dec2)
Dec2=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(cIntHiddenNode/2, activation="relu"))(Dec2)
DecOutIR=tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(1, activation="relu"))(Dec2)

model=tf.keras.Model(inputs=[InputCap,InputIR],outputs=[DecOutCap,DecOutIR])
代码
文本

加载数据

这里我们以及把48组电池划分为训练集 [trCap, trIR](29组),验证集 [vaCap, vaIR](10组),测试集 [teCap, teIR](9组)。

代码
文本
[9]
#load training data
trCap=pickle.load(open('/bohr/iseaaachen-ar4t/v1/trCap.p',"rb"))
trIR=pickle.load(open('/bohr/iseaaachen-ar4t/v1/trIR.p',"rb"))
vaCap=pickle.load(open('/bohr/iseaaachen-ar4t/v1/vaCap.p',"rb"))
vaIR=pickle.load(open('/bohr/iseaaachen-ar4t/v1/vaIR.p',"rb"))
teCap=pickle.load(open('/bohr/iseaaachen-ar4t/v1/teCap.p',"rb"))
teIR=pickle.load(open('/bohr/iseaaachen-ar4t/v1/teIR.p',"rb"))
代码
文本
[10]
print(len(trCap),len(trIR),len(vaCap),len(vaIR),len(teCap),len(teIR))
29 29 10 10 9 9
代码
文本

展示测试集的数据

代码
文本
[36]
for i in range(9):
plt.plot(teCap[i],label=f'test-{i+1}')
plt.xlabel('Cycle')
plt.ylabel('Capacity (Ah)')
plt.title('Test group battery capacity fade')
plt.legend()
plt.show()
代码
文本
[37]
for i in range(9):
plt.plot(teIR[i]*1000,label=f'test-{i+1}')
plt.xlabel('Cycle')
plt.ylabel('Internal Resistance (mΩ)')
plt.title('Test group internal resistance growth')
plt.legend()
plt.show()
代码
文本

数据处理

正如我们前面提到的,由于我们需要使用不同数量的前期循环数据预测后面的衰减/增长曲线,我们需要对分割后的数据进行填充和组装。下面的BuildSeqs函数就是如此,这里我们从前20圈开始,一直到倒数20圈为止,对每个电池的曲线依次切割。前期循环数据中的容量和阻抗作为输入,后续的数据每四个点取一个作为输出,这样做的原因是一方面可以减少计算量,另一个方面,由于数据本身就是插值得出,选择少量的点也不会对精度造成太大损失。同时对数据做了归一化处理。

代码
文本
[13]
#function to build training data
def BuildSeqs(Cap,IR):
#declare list for input capacity and input IR as ls1 and ls2
#declare list for output capacity and output IR as ls3 and ls4
ls1,ls2,ls3,ls4=list(),list(),list(),list()
for SelectCap,SelectIR in zip(Cap,IR):
if(len(SelectIR)<len(SelectCap)):
SelectCap=SelectCap[0:len(SelectIR)]
elif(len(SelectCap)<len(SelectIR)):
SelectIR=SelectIR[0:len(SelectCap)]
SelectIR=SelectIR/0.04*100
SelectCap=SelectCap/1.85*100
x_lst=[]
x_lst2=[]
y_lst=[]
y_lst2=[]
for i in range(20,len(SelectIR)-20,1):
splitPos = i
inputSeq=SelectCap[0:splitPos]
x_lst.append(inputSeq.reshape(-1,1))
inputSeq2=SelectIR[0:splitPos]
x_lst2.append(inputSeq2.reshape(-1,1))
OutputSeq=SelectCap[splitPos-1::4].tolist()
y_lst.append(OutputSeq)
OutputSeq2=SelectIR[splitPos-1::4].tolist()
y_lst2.append(OutputSeq2)
#zero padding
Proc_X=tf.keras.preprocessing.sequence.pad_sequences(x_lst,maxlen=cIntInputSeqLen,dtype='float64',padding='pre',value=0)
Proc_X2=tf.keras.preprocessing.sequence.pad_sequences(x_lst2,maxlen=cIntInputSeqLen,dtype='float64',padding='pre',value=0)
Proc_Y1=tf.keras.preprocessing.sequence.pad_sequences(y_lst,maxlen=cIntOutputSeqLen,dtype='float64',padding='post',value=0)
Proc_Y2=tf.keras.preprocessing.sequence.pad_sequences(y_lst2,maxlen=cIntOutputSeqLen,dtype='float64',padding='post',value=0)
Proc_X=Proc_X.reshape(-1,cIntInputSeqLen,cIntProcFeatures)
Proc_X2=Proc_X2.reshape(-1,cIntInputSeqLen,cIntProcFeatures)
Proc_Y1=Proc_Y1.reshape(-1,cIntOutputSeqLen,cIntProcFeatures)
Proc_Y2=Proc_Y2.reshape(-1,cIntOutputSeqLen,cIntProcFeatures)
for a,b,c,d in zip(Proc_X,Proc_X2,Proc_Y1,Proc_Y2):
ls1.append(a)
ls2.append(b)
ls3.append(c)
ls4.append(d)
return (np.array(ls1,dtype=float),np.array(ls2,dtype=float)),(np.array(ls3,dtype=float),np.array(ls4,dtype=float))
代码
文本

然后我们通过定义的BuildSeqs函数处理训练数据集和验证集。

模型训练

在模型训练的部分,我们分三个阶段对模型进行训练,这样做的目的是为了加快收敛。在每个阶段完成后,会保存训练权重。我们可以自动或手动选择训练效果最好的权重进入下一个阶段的训练。请注意,这里如果重新训练的话,需要的时间可能较长(>3h)

代码
文本
[15]
#generate training data
x0,y0=BuildSeqs(trCap,trIR)
x1,y1=BuildSeqs(vaCap,vaIR)
#set checkpoint path
checkpoint_path0 = "capir/weight0_{epoch:04d}-{loss:.2f}-{val_loss:.2f}.hdf5"
checkpoint_path1 = "capir/weight1_{epoch:04d}-{loss:.2f}-{val_loss:.2f}.hdf5"
checkpoint_path2 = "capir/weight2_{epoch:04d}-{loss:.2f}-{val_loss:.2f}.hdf5"
#define checkpoint and early stopping callback
cp_callback0 = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path0, save_weights_only=True,verbose=1,save_freq='epoch')
cp_callback1 = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path1, save_weights_only=True,verbose=1,save_freq='epoch')
cp_callback2 = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_path2, save_weights_only=True,verbose=1,save_freq='epoch')
callback2 = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=32,restore_best_weights=True)
#train model for stage 0
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),loss=CustomLoss.MaskedMAE,metrics=[],loss_weights=([1.0,0.0]))
model.fit(x0,y0,batch_size=384,epochs=300,verbose=2,validation_data=(x1,y1),shuffle=True,callbacks=[cp_callback0,callback2])
model.save("2CapIR2_stg0")
#freeze weights for stage 1
for idx in range(len(model.layers)):
model.layers[idx].trainable=False
model.layers[11].trainable=True
model.layers[13].trainable=True
model.layers[15].trainable=True
model.layers[17].trainable=True
model.layers[19].trainable=True
model.layers[21].trainable=True
model.layers[23].trainable=True
已隐藏输出
代码
文本
[16]
#train model for stage 1
#sometimes need load checkpoint because the local best solution may not be the best solution for remaining stages.
#model.load_weights('capir/weight0_best.hdf5')
model.load_weights('/bohr/iseaaachen-ar4t/v1/capir/weight0_0118-0.62-0.60.hdf5')
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4),loss=CustomLoss.MaskedMAE,metrics=[],loss_weights=([0.0,1.0]))
model.fit(x0,y0,batch_size=384,epochs=300,verbose=2,validation_data=(x1,y1),shuffle=True,callbacks=[cp_callback1,callback2])
model.save("2CapIR2_stg1")
#defreeze all weights for stage 2
for idx in range(len(model.layers)):
model.layers[idx ].trainable=True

已隐藏输出
代码
文本
[18]
#train model for stage 2
#model.load_weights('capir/weight1_best.hdf5')
model.load_weights('/bohr/iseaaachen-ar4t/v1/capir/weight1_0209-0.49-0.49.hdf5')
model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),loss=CustomLoss.MaskedMAE,metrics=[],loss_weights=([1.0,1.0]))
#model.load_weights('capir/weight2_best.hdf5')
model.fit(x0,y0,batch_size=512,epochs=300,verbose=1,validation_data=(x1,y1),shuffle=True,callbacks=[cp_callback2,callback2])

#save model
model.save("2CapIR2")
tf.keras.utils.plot_model(model,"2CapIR2.png",show_shapes=True,expand_nested=True)
已隐藏输出
代码
文本

模型预测

我们可以使用刚刚训练完成的模型,也可以加载已经训练完成的模型参数,直接进行预测。

代码
文本
[ ]
from tensorflow.keras.models import load_model
model = load_model('/bohr/iseaaachen-ar4t/v1/2CapIR2')
代码
文本

同样,我们使用BuildSeqs函数对测试集数据进行处理。x2为模型的输入,即前期的容量和内阻序列数据。y2为对应的真实值,即后续循环的容量衰减和内阻增长序列。prediction为我们预测出的后续循环的数据。

代码
文本
[38]
x2,y2=BuildSeqs(teCap,teIR)
prediction = model.predict(x2)
81/81 [==============================] - 56s 696ms/step
代码
文本

我们将预测的结果与实际值放在一起比较,可以看到,在使用少量前期数据做预测时,模型对于两个任务都能做到较为有效的预测。这里我们使用测试集电池1的前50圈的数据做输入为例。注意这里我们已经把容量和内阻的单位转换为正常单位。(出于训练时间的考虑,这里我们的结果比最优结果稍差,读者有兴趣的话可以继续调整参数或继续训练)

代码
文本
[44]
for ca in [30]:
case = ca
lab = 0
x2c = []
y2c = []
pre_c = []
for i in x2[lab][case]:
if i != 0:
x2c.append(i*1.85/100)
for i in y2[lab][case]:
if i != 0:
y2c.append(i*1.85/100)
ind = y2[lab][case].tolist().index(i)
pre_c.append(prediction[lab][case][ind]*1.85/100)
num = [4*i+len(x2c) for i in range(len(y2c))]
#plt.plot(x2c,'b')
plt.plot(num[1:],pre_c[1:],label='Prediction Value')

plt.plot(teCap[0],label='Actual Value')
plt.xlabel('cycle')
plt.ylabel('Capacity(Ah)')
plt.title('Capacity fade predition')
plt.legend()
plt.show()

def RMSEtest(y_true, y_pred):
se = 0.0
for i,j in zip(y_true, y_pred):
se += (i - j)**2
rmse = (se/len(y_true))**0.5
return rmse

def MAPEtest(y_true, y_pred):
se = 0.0
for i,j in zip(y_true, y_pred):
se += abs((i - j)/i)
mape = (se/len(y_true))*100
return mape
print('RMSE:',RMSEtest(teCap[0][num[1]:],pre_c[1:]))
print('MAPE:',MAPEtest(teCap[0][num[1]:],pre_c[1:]),'%')
RMSE: [0.16555224]

MAPE: [3.6057513] %
代码
文本
[45]
for ca in [30]:
case = ca
lab = 1
x2c = []
y2c = []
pre_c = []
for i in x2[lab][case]:
if i != 0:
x2c.append(i*0.04*10)
#print(len(x2c))
for i in y2[lab][case]:
if i != 0:
y2c.append(i*0.04*10)
ind = y2[lab][case].tolist().index(i)
pre_c.append(prediction[lab][case][ind]*0.04*10)
#print(len(y2c))
num = [4*i+len(x2c) for i in range(len(y2c))]
#plt.plot(x2c,'b')
#plt.plot(num,y2c,label=f'{ca+20}')
plt.plot(num,pre_c,label='Prediction Value')

plt.plot(teIR[0]*1000,label='Actual Value')
plt.xlabel('cycle')
plt.ylabel('IR(mΩ)')
plt.title('Internal Resistance fade predition')
plt.legend()
plt.show()

print('RMSE:',RMSEtest(teIR[0][num[0]:]*1000,pre_c))
print('MAPE:',MAPEtest(teIR[0][num[0]:]*1000,pre_c),'%')
RMSE: [3.786972]

MAPE: [3.7053008] %
代码
文本
[54]
from numba import cuda
cuda.select_device(0)
cuda.close()
代码
文本
AI+电芯
AI+电芯
点个赞吧
本文被以下合集收录
电芯
Piloteye
更新于 2024-07-22
17 篇3 人关注
推荐阅读
公开
基于卷积神经网络的锂电池老化模式分析
中文锂电池Deep Learning
中文锂电池Deep Learning
KaiqiYang
发布于 2023-09-01
6 转存文件
公开
通过电压特征来预测电芯剩余寿命
中文锂电池
中文锂电池
KaiqiYang
发布于 2023-08-22
4 赞7 转存文件