蛋糕图片
正在给蛋糕浇上蜂蜜
ntainer" style="display: none">
文章

编程与开发

编程技术与开发杂谈

编程与开发

虚拟环境和Anaconda

安装的 python,主要包含了 Lib 库(包含标准库和 site-packages 第三方包存储位置)、Scripts(包含 pip.exe 包管理器)以及 python.exe 解释器;安装的时候还安装了 python 启动器(用于管理多个不同版本的 python)。虚拟环境即将上面的东西复制一份(一般情况下不含标准库,标准库通过符号链接或直接引用系统 python 安装路径下的标准库;但 anaconda 出于环境隔离的考虑,会将标准库一并复制;以及会将解释器和包管理器统一放在 Scripts 路径下,主要是为了方便添加环境变量)。

虚拟环境本质上是对解释器、包管理器的一种隔离式复制;虚拟环境的激活和去激活就是在环境变量 Path 中添加或去除虚拟环境的解释器路径,相当于对安装位置默认的路径进行截胡。在虚拟环境下安装第三方库,就会安装到虚拟环境的路径下了。

base 是 Anaconda 安装后默认创建的虚拟环境,为了让用户开箱即用,默认在终端启动时就激活 base 环境。如果不激活任何虚拟环境,就在操作系统本地的环境(也叫 “系统 Python” 或 “裸机环境”)中,容易造成 依赖污染、库版本冲突、难以迁移项目 Denham 问题,这正是虚拟环境存在的原因。Anaconda Prompt 本质上是一个 CMD 命令行 + 初始化脚本。它启动时根据使用的 shell 调用不同的脚本,然后设置好 PATH 等环境变量,并激活 (base) 环境。

Pytorch 使用

pytorch是基于python的开源机器学习框架,机器学习的基本流程包括:数据集准备和加载、网络模型构建、前向传播、计算损失、反向传播、使用优化器进行梯度下降、保存训练模型。pytorch对其中的每个步骤都提供了丰富的API,在使用框架构建机器学习系统时,每个步骤主要的关注点如下

  • 数据集准备和加载:数据集的组织方式以及提供img和label的访问接口、使用transforms进行预处理(ToTensor、Resize、Normalize、Compose等)、加载方式(batch_szie、shuffle、num_workers)。另外,pytorch提供了图像分类、目标检测、语义分割等任务的常见公开数据集可供直接使用。
  • 网络模型构建:继承nn.Module类并使用nn中提供的各种层并配置相应的参数来构建网络,常用的层包括Conv、MaxPool、LInear、ReLU、Sigmoid、Flatten等等,可以使用Sequential将它们串联起来。另外,pytorch图像分类、目标检测、语义分割等任务的经典网络模型可供直接使用。
  • 前向传播:将输入传入构建的模型并得到预测输出并记录计算图。计算图即用于记录从输入一步步算到输出的过程的数据结构。
  • 计算损失:使用nn.loss并指定损失函数,计算输出和目标的损失,常见的损失函数包括L1、MSE、交叉熵等。
  • 反向传播:调用loss的backward方法,通过链式法则计算损失函数对模型参数的梯度。pytorch中通过autograd机制实现反向传播,当参数tensor的requires_grad属性为True时,在调用backward方式并传入模型参数时,就会自动追踪并计算梯度。
  • 梯度下降:使用optimizer并选用优化器算法,根据反向传播计算出的梯度更新模型参数,常见的优化器算法包括SGD、Adam等。
  • 模型保存:推荐用save方法保存模型的state_dict只保存模型参数,可以减小模型大小;加载时使用load_state_dict即可。

pytorch默认会对所有tensor记录计算图以便反向传播求梯度,但并不是所有操作都需要梯度(全部追踪会浪费内存和计算),只有用于训练模型的前向传播、损失计算、反向传播以及其它需要对模型参数求导的地方需要追踪梯度,其它如模型推理、日志保存、指标计算等都不需要追踪梯度。通过with torch.no_grad():这个上下文管理器,with代码块中的语句会临时关闭梯度追踪。

pytorch默认会对梯度进行累加,通常来说需要在每轮执行完step的参数更新到下一轮backward之间,调用optimizer的zero_grad方法清除之前的梯度以确保只记录当轮梯度。只有一些特殊情况下(如显存不足需要累积多个小批次的梯度后再更新参数),才会每N轮后清零一次梯度。

在torch.cuda.is_available() = True的情况下,推荐使用gpu进行训练,具体操作上即将模型和输入数据(参与计算图运算的数据)迁移到gpu上,使用.cuda()方法可迁移到默认gpu上,使用.to(device)方法可以更灵活地迁移到目标设备(支持CPU和多GPU)。另外,可使用torch.cuda.empty_cache()手动释放未使用的张量缓存。

另外,除了pytorch框架,常用tensorboard来可视化机器学习实验的结果,监控训练指标等。

Numpy 使用

numpy以具有维度的数组来组织数据,最常用的是python语法中的切片操作,如array[1::2, 3]表示第二行开始到最后一行每隔一行,第四列;另外注意numpy的广播机制,很方便但也容易埋坑。shape方法查看形状,reshape方法重塑形状,如三维数组(2, 4,3)可以理解为两个四行三列的二维数组叠起来,dtype属性查看数据类型或定义时设置数据类型;zeros、ones、full、eys、random、randn、radint方法填入形状参数可以填充数组元素,concatenate方法可以合并两个数组。

设计模式

设计模式是面向对象系统的代码设计思想,核心思想是提供可复用的解决方案,让代码更易维护、扩展和理解。设计模式独立于具体编程语言,不同编程语言特性差异很大,有的语言本身就是为了贯彻某个设计原则而被开发出来的,有的语言提供了针对某些设计模式的语法糖,同一种设计模式在不同语言中也可能有不同的实现方式。

代码的设计其实自底向上分为三层,最基本的是低耦合高内聚的设计思想,往上是面向对象的设计原则,再往上才是各种设计模式。设计模式依赖接口这个抽象规范,接口是抽象方法的集合,在底层实现接口时,必须按照接口给定的调用方式来实现,而对高层模块则隐藏了类的内部实现。

面向对象的设计原则

面向对象的三大法则是封装继承多态,三者是递进关系。封装就是有意识地把对象的属性和方法分为对内的和对外的;继承是为了提高代码的复用性,子类可以直接复用父类的属性和方法,并可以实现新功能或覆写父类方法的实现;多态指同一操作作用于不同对象时,会产生不同的执行结果。

面向对象的设计原则包括:

  • 开放封闭原则:对扩展开放,对修改关闭,即支持加新功能,但最好不要修改已经成型的代码。
  • 里氏替换原则:引用父类的对象要能透明地使用其子类的对象,即子类和父类的方法,实现上可以不同,但输入和返回需要一致。
  • 依赖倒置原则:高层模块不应该依赖底层模块,两者应该依赖接口这个抽象。抽象不应该依赖细节,细节应该依赖抽象,即要针对接口而不是针对实现编程。
  • 接口隔离原则:客户端不应该依赖那些它不需要的接口,因此要使用多个专门的接口而不是单一的总接口。
  • 单一职责原则:一个类只负责一项职责,不要存在多个导致类变更的原因。

设计模式分类

设计模式大致分为创建者模式、结构型模式、行为型模式。创建者模式解决如何创建对象的问题;结构型解决对象之间怎么组织的问题;行为型模式解决如何实现方法的问题。

  • 创建者模式:简单工厂模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式
  • 结构性模式:适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式、代理模式
  • 行为型模式:解释器模式、责任链模式、命令模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、访问者模式、模板方法模式

创建者模式

简单工厂模式:通过工厂类负责创建产品类的实例,隐藏创建对象的实现,但违反了单一职责原则和开闭原则。

工厂方法模式:将工厂分为抽象工厂角色和具体工厂角色,一个工厂角色只负责创建一个产品类的实例。

抽象工厂模式:每一个具体工厂都生产一套产品而不是一个,有利于维护产品之间的约束关系,但难以支持新种类的抽象产品。

建造者模式:用指挥者角色以同样的构建过程创建不同的表示,有利于约束构建顺序。

原型模式:对于需要被复制的类,从内部提供克隆的接口方法,使得在某些属性和方法对外部不可见的情况下,外部也能对类进行复制。

单例模式:保证一个类只有一个实例并提供一个全局的访问点,相当于全局变量的同时防止了命名空间被污染。单例模式分饿汉式和懒汉式,饿汉式即在类加载时进行实例化,懒汉式即在第一次使用时进行实例化。

一般来说,以简单工厂或工厂方法开始,当发现设计需要更大的灵活性时,再向抽象工厂或建造者演化。

结构性模式

适配器模式:使得原本由于接口不兼容而不能一起工作的类可以一起工作,实现方式有多继承(类适配器)和组合(对象适配器)两种,多继承即同时继承多个类,组合即将要适配的类作为适配器的属性,以在适配器中调用需要的属性和方法。

桥接模式:将一个实物的两个维度分离成抽象和实现,将继承关系转换为组合关系,两个维度都可以独立扩展,提高了扩展的灵活性。

组合模式:将对象组合成树形结构,使得用户对单个对象和组合对象的使用具有一致性,角色包括抽象组件、叶子组件、复合组件。

装饰模式:在不改变原有对象结构的前提下,动态地给对象添加额外功能。它通过创建一个包装类(装饰器)来包裹原始对象,允许在调用原始对象方法的前后添加新的行为,同时保持接口的一致性。

外观模式:定义高层接口来统一调用子系统中的功能,使得子系统组合起来的功能更易于使用,实质是多一层封装。

享元模式:通过共享技术减少系统中对象的数量,节省内存空间并提高性能;适用于存在大量相似或相同对象的场景,通过复用已存在的对象来避免重复创建,仅在必要时才创建新对象。

代理模式:为真实对象提供一种代理来以控制这个对象的访问,角色包括抽象实体(作为接口)、实体(真实对象)、代理(和真实对象使用一致的代理对象);常见的应用场景如远程代理、虚代理、保护代理。

行为型模式

解释器模式:一般用于做编译器,解释器是负责解释文法规则的模块,解释器模式是一种语法解释器的开发框架。

责任链模式:使多个对象连成一条链并沿着这条链传递请求,链上的对象都有机会处理请求,避免在客户端处理发送者和接收者之间的耦合关系,一个对象无需知道是其它哪一个对象处理其请求,要求链上的对象有同样的处理接口并存储链的下一级对象。

命令模式:一般用于做桌面程序、命令行工具等,将请求转换为一个包含与请求相关的所有信息的独立对象。将对象的具体实现(行为的实现者)通过接口与业务逻辑(行为的请求者)分离。

迭代器模式:一般用于实现迭代器这种数据结构,将遍历逻辑与聚合对象分离,使得同一聚合对象可以支持多种遍历方式,同时遍历方式的变化不会影响聚合对象本身。

中介者模式:减少对象之间混乱无序的依赖关系,限制对象之间的直接交互,迫使它们通过中介者对象进行合作。

备忘录模式:允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态,用于实现对象的 “快照” 功能和撤销操作。核心结构包括原发器(可以创建备忘录或从备忘录恢复)、备忘录(存储原发器的内部状态,对除了原发器之外的外部隐藏细节)、负责人(管理备忘录的存储和获取,但不直接操作备忘录中的状态)。

观察者模式:又称发布订阅模式,当一个对象的状态改变时,所有依赖它的对象都得到通知并被自动更新,角色包括抽象发布者、具体发布者、抽象观察者、具体观察者。

状态模式:将对象在不同状态下的行为封装到独立的状态类中,使得对象的状态变化时,其行为能自动切换到对应状态的实现,从而避免使用大量的条件判断。内部状态需抽象,行为与状态匹配,不同状态关联不同行为导致不同表现效果。状态模式是状态机思想的一种实现方式。

策略模式:封装一系列可相互替换的算法,算法独立于使用它的客户而变化,即提供相同行为的不同实现,角色包括抽象策略、具体策略和上下文,但客户必须了解不同的策略。

访问者模式:在不修改已有类的前提下,为一组不同类型的对象添加新的操作。它通过将操作逻辑与对象结构分离,使得操作可以独立于对象而变化。定义一个访问者对象,来访问一组不同类型的元素,并对每个元素执行特定操作,而无需在元素类中直接实现这些操作。

模板方法模式:定义一个操作中的算法骨架,而将一些步骤延迟到子类中,可以在不改变算法结构的情况下可重定义操作的某些步骤。角色包括抽象的原子操作/钩子操作(实现模板方法作为算法的骨架)和具体类(实现原子操作)。

容器技术

通过操作系统级虚拟化,将应用及其依赖打包成容器,实现 “一次构建,到处运行”;但在实现上不虚拟整个操作系统,只虚拟应用运行所需的库、依赖、配置文件等必要组件,并与宿主机共享操作系统内核,比虚拟机更轻量化。比较出名的容器技术有docker、k8s。

docker简单来说就是用容器化技术给应用程序封装独立的运行环境,每个运行环境就是一个容器,运行容器的计算机称为宿主机。镜像是容器的模板。每个docker都运行在独立的虚拟环境中,容器的网络与宿主机是隔离的。

参考资料

docker简单实践

本文由作者按照 CC BY 4.0 进行授权
/body>