作业二——读书笔记:软件设计原则&设计模式
1、参考书籍
| 名称 | 出处 | 作者 |
|---|---|---|
| 《软件秘笈:设计模式那点事》 |
https://pan.baidu.com/s/1ntObMI1?_at_=1646400798296 | 郑阿奇 |
2、设计原则
| 设计模式 | 内容 | 开发中的体会 |
| 里氏代换原则 |
一个可以接受基类对象的地方必然可以接受一个子类对象,即能调用基类的函数也一定能调用子类 |
在实际的开发中,该原则是十分重要的。java中的接口抽象函数,C++中的虚函数基本都是该思想的衍生。以接口为例,在实际开发的过程中基本上都是团队开发的,不同成员的开发进度和开发任务都不同。为了加强团队间的信息沟通,接口就成了所必须的功能。因为有了接口,不同成员所写的代码才能相互兼容,不同成员的也能更高效地debug |
| 开闭原则 | 软件实体对扩展是开放的,但对修改是关闭的。即基类是不可修改只可继承的 | 在开发中开闭原则个人感觉体现在兼容性上,只有严格保证开闭原则才能保证兼容性。这点在供人调用的库上尤为明显。以深度学习库keras为例,因为keras的层基类keras.layer从版本之初到现在都没有改变过才使得keras在迭代的过程中依然保证了对旧代码的兼容 |
| 依赖倒转原则 | 要针对抽象层编程,而不要针对具体类编程。即应该对实现对象进行高度抽象 | 该思想作为软件工程的学生体会尤为明显,软件工程学、高级软件工程、需求工厂甚至数据库的课程中都将类图作为一项考核要求。绘制类图的过程就是对所需要类的高度抽象,反映到实际开发中高度抽象再继承的实现方法相比较针对具体类编程代码量能降低一个量级 |
| 单一职责原则 | 类的职责要单一,不能将太多的职责放在一个类中 | 在实际开发中,对该原则的体验也感触颇深。以写一个机器人类为例子,一开始会把机器人的所有功能都塞进一个类里。但后续又想实现一个狗狗类,狗狗和机器人本身差别极大继承机器人类再做修改工作量不亚于从头开始写。但狗狗和机器人类走路,转身这种基础功能又有相同的逻辑的功能如果一开始就能拆分出来可以大大减少狗狗类的代码量 |
| 接口隔离原则 | 使用多个专门的接口来取代一个统一的接口 | 在这点上类似于单一职责原则,不过主要是体现在子类继承基类上。如果严格贯彻接口隔离原则,在继承实现子类时能减少代码量。比如轮船类如果继承自火车类,需要把火车在陆地走的方法重写成轮船在海上走,此时如果航行地点单独作为一个方法那么就可以单独只改进这个方法 |
| 合成复用原则 | 在系统中应该尽量多使用组合和聚合关联关系,尽量少使 | 个人认为和单一职责原则是互补的,如果有腿类,手类等单一职责类,那狗狗类可以看作四个腿类拼接,机器人类则是两个手类+两个腿类。将复杂的代码构建过程简化成搭积木一样的模块化方法,对修复bug和快速构建很有帮助。 |
| 迪米特法则 | 一个软件实体对其他实体的引用越少越好,或者说如果两给类不必直接沟通而是通过第三方类沟通 | 该法则可以说是写最简洁代码的必备法则,虽然在书写代码时不能直接体现,但贯彻该法则能让代码耦合度极低。最大的感触就是代码修改时的工作量大幅下降,并且相互阅读代码时理解难度也大幅下降 |
2、设计模式
我将以工厂设计模式为例讲一下我对设计模式的体会。
大体上工厂设计模式分为简单工厂模式、工厂方法模式和抽象工厂模式三种
简单工厂模式:简单工厂模式是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,但不属于23种GOF设计模式之一。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。即一个类对应与一个具体的对象
工厂方法模式:这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。即通过某个公共类实现对产品类的初始化,keras中的mode类就是基于该设计模式的
抽象工厂模式:抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。这是实际开发中经常接触的一种模式。在python和java中常用的装饰器就源于该设计模式。
3、实际应用
基于抽象工厂设计模式和设计原则,我利用keras实现了一个多头注意力机制[1]的代码
class MultiHead(keras.layers.Layer): #标准多头注意力层 def __init__(self, n_head=1, #头的数量 head_dim=None,#每个头的维度 drop_rate=0.5, #drop比例 use_Time_shift=False,#是否使用time-shift mask_future=False,#是否mask掉未来 #dense层的参数,参考keras的文档https://keras.io/zh/layers/core/#dense att_activation=None, kernel_initializer=keras.initializers.TruncatedNormal(stddev=0.02),#bert的初始化策略 att_use_bias=False, bias_initializer='zeros', kernel_regularizer=None, bias_regularizer=None, activity_regularizer=None, kernel_constraint=None, bias_constraint=None, attention_scale=True, **kwargs): super(MultiHead, self).__init__(**kwargs) self.head_dim = head_dim self.mask=mask_future self.n_head = n_head self.drop_rate=drop_rate self.use_bias=att_use_bias self.kernel_initializer= keras.initializers.get(kernel_initializer) self.use_Time_shift=use_Time_shift self.attention_scale=attention_scale self.activation=keras.layers.Activation(att_activation) self.bias_initializer=keras.initializers.get(bias_initializer) self.kernel_regularizer=keras.regularizers.get(kernel_regularizer) self.bias_regularizer=keras.regularizers.get(bias_regularizer) self.activity_regularizer=keras.regularizers.get(activity_regularizer) self.kernel_constraint=keras.constraints.get(kernel_constraint) self.bias_constraint=keras.constraints.get(bias_constraint) def get_dense(self,dims): #获得全连接层 dense=keras.layers.Dense(dims, activation=self.activation,use_bias=self.use_bias, kernel_initializer=self.kernel_initializer, bias_initializer=self.bias_initializer, kernel_regularizer=self.kernel_regularizer, bias_regularizer=self.bias_regularizer, activity_regularizer=self.activity_regularizer, kernel_constraint=self.kernel_constraint, bias_constraint=self.bias_constraint) return dense def switch_weights(self,dims,name,input_shape): return self.add_weight( name=name, shape=(input_shape[0][-1], dims), initializer=self.kernel_regularizer, ) def set_main_weights(self,input_shape): #获取self-attention的主要权重,有的attention变种主要改这一部分的权重获取 self.wq = self.switch_weights(self.n_head * self.head_dim,'wq',input_shape) self.wk = self.switch_weights(self.n_head * self.head_dim,'wk',input_shape) self.wv = self.switch_weights(self.n_head * self.head_dim,'wv',input_shape) # [n, step, h*h_dim] self.o_dense = self.get_dense(self.n_head * self.head_dim) self.o_drop = keras.layers.Dropout(rate=self.drop_rate) if self.attention_scale==False: self.wq=self.wq/(self.head_dim**0.5) self.wk=self.wk/(self.head_dim**0.5) def set_other_weights(self,input_shape): #获得其他权重,方便attention变种进行修改 return 0 def define_weights(self,input_shape): #一个小build函数 self.set_main_weights(input_shape) self.set_other_weights(input_shape) def build(self,input_shape): #调用define_weight申请权重 self.define_weights(input_shape) super(MultiHead, self).build(input_shape) def time_shift_pad(self,x): #获取time-shift return K.temporal_padding(x,(1,0)) def mask_martic(self,k): #获取常规的mask矩阵 seq_len=K.shape(k)[-2] idxs = K.arange(0, seq_len) mask = idxs[None, :] <= idxs[:, None] mask = K.cast(mask, K.floatx()) return 1-mask def time_shift(self,x): d=x.shape[-1] x=K.concatenate([self.time_shift_pad(x)[:,:-1,:d//2],x[:,:,d//2:]],-1) return x def compute_qkv(self,q,k,v): _q =tf.matmul(q,self.wq) # [n, q_step, h*h_dim] _k, _v = tf.matmul(k,self.wk), tf.matmul(v,self.wv) return _q,_k,_v def get_QKV_mat(self,q,k,v,mask): #获取QKV矩阵,通过改写这一层实现某些相对位置编码 _q,_k,_v=self.compute_qkv(q,k,v) q = self.split_heads(_q,mask) # [n, h, q_step, h_dim k, v = self.split_heads(_k,mask), self.split_heads(_v,mask) # [n, h, step, h_dim] return q,k,v def call(self, inputs,mask=None,**kwargs): if len(inputs)==4: q,k,v,LM_mask=inputs else: q,k,v=inputs[0],inputs[1],inputs[2] LM_mask=None if self.use_Time_shift: q=self.time_shift(q) k=self.time_shift(k) v=self.time_shift(v) _q, _k, _v=self.get_QKV_mat(q,k,v,mask) context = self.scaled_dot_product_attention(_q, _k, _v,mask=mask,LM_mask=LM_mask,inputs=[q,k,v]) # [n, q_step, h*dv] context=self.recover_heads(context) o = self.o_dense(context) # [n, step, dim] o = self.o_drop(o) return o def split_heads(self, x,mask): #多头的情况 x =K.reshape(x, (-1, K.shape(x)[1], self.n_head, self.head_dim)) # [n, step, h, h_dim] return tf.transpose(x, perm=[0, 2, 1, 3]) # [n, h, step, h_dim] def recover_heads(self,context): context = tf.transpose(context, perm=[0, 2, 1, 3]) context = K.reshape(context, (K.shape(context)[0], K.shape(context)[1],self.n_head * self.head_dim)) return context def pay_QK(self,q,k,inputs): return tf.matmul(q, k, transpose_b=True) def pay_attention_V(self,attention,v,inputs): return tf.matmul(attention, v) def divide_dk(self,score,q,k): if self.attention_scale: dk = K.cast(K.shape(score)[-1], dtype=tf.float32) score=score/ (tf.math.sqrt(dk) + 1e-8) return score def apply_attention(self,score,v,mask,inputs): ''' q_mask, v_mask = None, None if v_mask!=None: q_mask, v_mask=mask[0],mask[1] score=sequence_masking(score, v_mask, '-inf', -1) q_mask=tf.expand_dims(q_mask,-1) q_mask=tf.cast(q_mask,score.dtype) ''' self.attention = keras.activations.softmax(score, axis=-1) # [n, h, q_step, step] #if q_mask!=None: # self.attention*=q_mask context = self.pay_attention_V(self.attention, v,inputs) # [n, h, q_step, step] @ [n, h, step, dv] = [n, h, q_step, dv] return context def scaled_dot_product_attention(self, q, k, v,compute_mask=None,mask=None,inputs=None,LM_mask=None): score =self.pay_QK(q, k,inputs) score=self.divide_dk(score,q,k) # [n, h_dim, q_step, step] if LM_mask!=None: score += mask * -1e9 elif self.mask==True: #mask LM_mask=self.mask_martic(score) score += LM_mask * -1e9 return self.apply_attention(score, v, mask,inputs) def get_config(self): config = { 'n_head': self.n_head, 'head_dim': self.head_dim, 'drop_rate': self.drop_rate, 'att_use_bias': self.use_bias, 'use_Time_shift':self.use_Time_shift, 'mask_future':self.mask, 'attention_scale':self.attention_scale, 'att_activation':keras.activations.serialize(self.activation), 'kernel_initializer':keras.initializers.serialize(self.kernel_initializer), 'bias_initializer':keras.initializers.serialize(self.bias_initializer), 'kernel_regularizer':keras.regularizers.serialize(self.kernel_regularizer), 'bias_regularizer':keras.regularizers.serialize(self.bias_regularizer), 'activity_regularizer':keras.regularizers.serialize(self.activity_regularizer), 'kernel_constraint':keras.constraints.serialize(self.kernel_constraint), 'bias_constraint':keras.constraints.serialize(self.bias_constraint), } base_config = super(MultiHead, self).get_config() return dict(list(base_config.items()) + list(config.items())) def compute_output_shape(self, input_shape): return input_shape
该实现受限于keras框架的规范主要基于接口隔离原则,虽然作为基类的实现较为复杂,很由于抽象了接口在实现子类时及其简便。就比如但需要实现该模型一个变种AFT[2]时的实现代码为
class AFT_full(MultiHead): def __init__(self,max_len=None,#最大长度 **kwargs): super(AFT_full, self).__init__(**kwargs) self.max_len=max_len def set_other_weights(self, input_shape): self.bias=self.add_weight(name='bias', shape=(self.max_len,self.max_len), initializer=self.kernel_initializer, trainable=True) def scaled_dot_product_attention(self, q, k, v,mask=None,LM_mask=None): seq_len_1 = K.shape(k)[-2] seq_len_2 = K.shape(q)[-2] w=self.bias[:seq_len_2,:seq_len_1] if LM_mask!=None: w += mask * -1e9 elif self.mask==True: mask=self.mask_martic(k) w+=mask*-1e9 k=K.exp(k) w=K.exp(w) temp = w@ (k* v) weighted = temp / (w @ k) return keras.activations.sigmoid(q)*weighted def get_config(self): config = { 'max_len':self.max_len, } base_config = super(AFT_full, self).get_config() return dict(list(base_config.items()) + list(config.items()))
不看看出,实现一个变种模型时由于做好了接口之间的隔离,在继承实现的时候时只需要对子模型和父模型的不同点进行重写即可。由于AFT作为变种模型共同点较多,反映到代码上便是只需要做一些简单的修改便能实现同等的功能
而我现在需要实现一个ROPE位置编码[3],需求是同时兼容[1]和[2]两个模型。通过抽象工厂模式的概念可以设计出下列代码
def Roformer(attention): class roformer(attention): def roform_position(self,x): batch_size,n_head,sequence_length,diemension=K.shape(x)[0],K.shape(x)[1],K.shape(x)[2],K.shape(x)[3] d=K.cast(diemension,dtype=K.floatx()) position_ids = K.arange(0, sequence_length, dtype=K.floatx())[None] indices = K.arange(0, diemension, dtype=K.floatx()) indices = K.pow(10000.0, -2 * indices / d) indices = tf.einsum('bn,d->bnd', position_ids, indices) sin=K.sin(indices) cos=K.cos(indices) x_=K.stack([-1*x[...,1::2],x[...,::2]],4) x_=K.reshape(x_,[batch_size,n_head,sequence_length,diemension]) return cos*x+sin*x_ def get_QKV_mat(self,q,k,v,mask): q,k,v=super(roformer, self).get_QKV_mat(q,k,v,mask) q,k=self.roform_position(q),self.roform_position(k) return q,k,v return roformer
从代码中不难看出,当要实现rope+AFT这一产品时,只需要把AFT作为具体的产品交由函数装饰,同理[1]的注意力模型也能达到同等的效果。由于不需要指定具体的模型,只需保证是基类的子类,因此该设计符合抽象工厂设计模式
当然也不难看出该模式同样遵循着里氏代换原则和接口隔离原则。由于前者,我们可以对[1]的所有变种使用该装饰器。由于后者,在装饰过程中实际所需修改的代码量只有短短几行
参考文献
[1] Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., ... & Polosukhin, I. (2017). Attention is all you need. Advances in neural information processing systems, 30
[2]Zhai, S., Talbott, W., Srivastava, N., Huang, C., Goh, H., Zhang, R., & Susskind, J. (2021). An attention free transformer. arXiv preprint arXiv:2105.14103.
[3] Su, J., Lu, Y., Pan, S., Wen, B., & Liu, Y. (2021). Roformer: Enhanced transformer with rotary position embedding. arXiv preprint arXiv:2104.09864.
截图


浙公网安备 33010602011771号