2024年8月

专题的目标与价值成效

什么是专题
  1. 公司或企业为了抓住业务机会或者解决痛点问题,而采取的具体的行动和举措

专题的目标分析

1.业务调研了解目标的预期

利用5W2H来进行专题分析

  1. what——是什么?目的是什么?作什么工作?
    • 专题是什么
    • 专题产生的背景是什么
    • 专题的目标是什么,要达到怎样的预期,包含那些主要的内容
  2. why——为什么?为什么要这么做?理由何在?原因是什么?
    • 为什么要做这个专题,什么原因导致想要做这个专题
    • 不做这个专题行不行,不做会产生什么影响
  3. when——何时?什么时间完成?什么时机最适宜?
    • 什么时间开始做
    • 什么时间完成
  4. where——何地?在哪里做?从哪里入手?
    • 办公地点,比如工位,驻场
    • 跨地域协作
  5. who——何人做?有谁来承担?谁来完成?谁负责?
    • 谁来做专题,谁来负责这个专题
    • 做这个专题的团队组织结构
  6. how——如何?如何提高效率?如何实施?方法怎么样?
    • 如何去做专题
    • 专题做的方法是什么,需要投入那些资源
  7. how much——多少?做到什么程度?数量如何?质量水平如何?费用产出如何?
    • 在预期的时间内,要做那些内容
    • 要投入那些资源,产生那些费用

分析图

2. 产品调研了解专题的范围
  1. 业务范围
    • 根据业务调研的情况,以及专题的目标,确定业务的范围与边界
  2. 系统范围
    1. 在老系统上开发
      • 了解老系统的价值主张,主要的业务功能模块
      • 梳理业务流程,整理出带有泳道的业务流程图,或者功能模块图
      • 结合专题的目标与业务范围,
        思考专题与老系统之间的交互与协作,与老系统人员一起做影响点分析评估风险,以及困难
      • 梳理专题与系统之间集成关系,以及依赖的上下系统和第三方外部系统,比如上下游系统,第三方外部系统,梳理交互协作方式与依赖关系
      • 跨团队,跨项目,以及第三方系统依赖,思考人员配合,协作,支持的可行性
    2. 全新的系统
      • 思考专题的入口,以及是否存在多端接入的情况,比如,Web系统,可以从浏览器和微信公众号上访问
        • Web网站
        • 微信公众号
      • 结合专题的目标与业务范围,思考是否依赖上下游系统,以及第三方外部系统,梳理交互协作方式与依赖关系
        • 爬虫
        • AI智能客服

专题的价值成效

​ 专题价值的量化指标,验证专题的价值主张是否被解决和满足

1.如何设定可被量化的指标
  1. 基于历史数据分析
  2. 根据专题的目标,分解目标,分解为多个因子
  3. 根据因子的完成情况,预测目标的改变
2. 关键成效结果
  1. 帮助就医用户,快速就医,效率提升50%
  2. 帮助了解成都医院的用户,查阅和收集信息的效率提升50%
3.阶段里程碑节点
  1. 收集医院信息的准确性,达到80%
  2. AI职能客服的正确回答率,达到80%
  3. AI职能客服的回答时间,控制在3s以内
  4. 上线1个月,用户量突破百人
4. 成本估算
  1. 开发人员,投入开发和UI设计人员,1个月
  2. AI职能客服
  3. 网站部署和微信公众号注册成本
  4. 咨询专家成本
5. 问题,假设,依赖,风险
  1. 爬虫收集医院信息的准确率偏低
  2. AI职能客服的正确回答率偏低
  3. 医院行业有很多的专业术语,与业务流程,如何清晰的进行转换
  4. 爬虫,爬取医院信息过多,是否会违法

Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)

在前边我们聊了数据结构的设计和剪贴板的数据操作,那么这些操作都还是比较倾向于数据相关的操作,那么我们现在就来聊聊基本的图形绘制以及图形状态管理。

关于
Canvas
简历编辑器项目的相关文章:

图形绘制

我们做项目还是需要从需求出发,首先我们需要明确我们要做的是简历编辑器,那么简历编辑器要求的图形类型并不需要很多,只需要 矩形、图片、富文本 图形即可,那么我们就可以简单将其抽象一下,我们只需要认为任何元素都是矩形就可以完成这件事了。

因为绘制矩阵是比较简单的,我们可以直接从数据结构来抽象这部分图形,图形元素基类的
x, y, width, height
属性是确定的,再加上还有层级结构,那么就再加一个
z
,此外由于需要标识图形,所以还需要给其设置一个
id

class Delta {
  public readonly id: string;
  protected x: number;
  protected y: number;
  protected z: number;
  protected width: number;
  protected height: number;
}

那么我们的图形肯定是有很多属性的,例如矩形是会存在背景、边框的大小和颜色,富文本也需要属性来绘制具体的内容,所以我们还需要一个对象来存储内容,而且我们是插件化的实现,具体的图形绘制应该是由插件本身来实现的,这部分内容需要子类来具体实现。

abstract class Delta {
  // ...
  public attrs: DeltaAttributes;
  public abstract drawing: (ctx: CanvasRenderingContext2D) => void;
}

那么绘制的时候,我们考虑分为两层绘制的方式,内层的
Canvas
是用来绘制具体图形的,这里预计需要实现增量更新,而外层的
Canvas
是用来绘制中间状态的,例如选中图形、多选、调整图形位置/大小等,在这里是会全量刷新的,并且后边可能会在这里绘制标尺。

在这里要注意一个很重要的问题,因为我们的
Canvas
并不是再是矢量图形,如果我们是在
1080P
的显示器上直接将编辑器的
width x height
设置到元素上,那是不会出什么问题的,但是如果此时是
2K
或者是
4K
的显示器的话,就会出现模糊的问题,所以我们需要取得
devicePixelRatio
即物理像素/设备独立像素,所以我们可以通过在
window
上取得这个值来控制
Canvas
元素的
size
属性。

this.canvas.width = width * ratio;
this.canvas.height = height * ratio;
this.canvas.style.width = width + "px";
this.canvas.style.height = height + "px";

此时我们还需要处理
resize
的问题,我们可以使用
resize-observer-polyfill
来实现这部分功能,但是需要注意的是我们的
width

height
必须要是整数,否则会导致编辑器的图形模糊。

private onResizeBasic = (entries: ResizeObserverEntry[]) => {
  // COMPAT: `onResize`会触发首次`render`
  const [entry] = entries;
  if (!entry) return void 0;
  // 置宏任务队列
  setTimeout(() => {
    const { width, height } = entry.contentRect;
    this.width = width;
    this.height = height;
    this.reset();
    this.editor.event.trigger(EDITOR_EVENT.RESIZE, { width, height });
  }, 0);
};

实际上我们在实现完整的图形编辑器的时候,可能并不是完整的矩形节点,例如绘制云形状的不规则图形,我们需要将相关节点坐标放置于
attrs
中,并且在实际绘制的过程中完成
Bezier
曲线的计算即可。但是实际上我们还需要注意到一个问题,当我们点击的时候如何判断这个点是在图形内还是图形外,如果是图形内则点击时需要选中节点,如果在图形外不会选中节点,那么因为我们是闭合图形,所以我们可以用射线法实现这个能力,我们将点向一个方向做射线,如果穿越的节点数量是奇数,说明点在内部图形,如果穿越的节点数量是偶数,则说明点在图形外部。

我们仅仅实现图形的绘制肯定是不行的,我们还需要实现图形的相关交互能力。在实现交互的过程中我遇到了一个比较棘手的问题,因为不存在
DOM
,所有的操作都是需要根据位置信息来计算的,比如选中图形后调整大小的点就需要在选中状态下并且点击的位置恰好是那几个点外加一定的偏移量,然后再根据
MouseMove
事件来调整图形大小,而实际上在这里的交互会非常多,包括多选、拖拽框选、
Hover
效果,都是根据
MouseDown

MouseMove

MouseUp
三个事件完成的,所以如何管理状态以及绘制
UI
交互就是个比较麻烦的问题,在这里我只能想到根据不同的状态来携带不同的
Payload
,进而绘制交互。

export enum CANVAS_OP {
  HOVER,
  RESIZE,
  TRANSLATE,
  FRAME_SELECT,
}
export enum CANVAS_STATE {
  OP = 10,
  HOVER = 11,
  RESIZE = 12,
  LANDING_POINT = 13,
  OP_RECT = 14,
}
export type SelectionState = {
  [CANVAS_STATE.OP]?:
    | CANVAS_OP.HOVER
    | CANVAS_OP.RESIZE
    | CANVAS_OP.TRANSLATE
    | CANVAS_OP.FRAME_SELECT
    | null;
  [CANVAS_STATE.HOVER]?: string | null;
  [CANVAS_STATE.RESIZE]?: RESIZE_TYPE | null;
  [CANVAS_STATE.LANDING_POINT]?: Point | null;
  [CANVAS_STATE.OP_RECT]?: Range | null;
};

状态管理

在实现交互的时候,我思考了很久应该如何比较好的实现这个能力,因为上边也说了这里是没有
DOM
的,所以最开始的时候我通过
MouseDown

MouseMove

MouseUp
实现了一个非常混乱的状态管理,完全是基于事件的触发然后执行相关副作用从而调用
Mask Canvas
图层的方法进行重新绘制。

const point = this.editor.canvas.getState(CANVAS_STATE.LANDING_POINT);
const opType = this.editor.canvas.getState(CANVAS_STATE.OP);
// ...
this.editor.canvas.setState(CANVAS_STATE.HOVER, delta.id);
this.editor.canvas.setState(CANVAS_STATE.RESIZE, state);
this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.RESIZE);
this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.TRANSLATE);
this.editor.canvas.setState(CANVAS_STATE.OP, CANVAS_OP.FRAME_SELECT);
// ...
this.editor.canvas.setState(CANVAS_STATE.LANDING_POINT, new Point(e.offsetX, e.offsetY));
this.editor.canvas.setState(CANVAS_STATE.LANDING_POINT, null);
this.editor.canvas.setState(CANVAS_STATE.OP_RECT, null);
this.editor.canvas.setState(CANVAS_STATE.OP, null);
// ...

再后来我觉得这样的代码根本没有办法维护,所以改动了一下,将我所需要的状态全部都存储到一个
Store
中,通过我自定义的事件管理来通知状态的改变,最终通过状态改变的类型来严格控制将要绘制的内容,也算是将相关的逻辑抽象了一层,只不过在这里相当于是我维护了大量的状态,而且这些状态是相互关联的,所以会有很多的
if/else
去处理不同类型的状态改变,而且因为很多方法会比较复杂,传递了多层,导致状态管理虽然比之前好了一些可以明确知道状态是因为哪里导致变化的,但是实际上依旧不容易维护。

export const CANVAS_STATE = {
  OP: "OP",
  RECT: "RECT",
  HOVER: "HOVER",
  RESIZE: "RESIZE",
  LANDING: "LANDING",
} as const;

export type CanvasOp = keyof typeof CANVAS_OP;
export type ResizeType = keyof typeof RESIZE_TYPE;
export type CanvasStore = {
  [RESIZE_TYPE.L]?: Range | null;
  [RESIZE_TYPE.R]?: Range | null;
  [RESIZE_TYPE.T]?: Range | null;
  [RESIZE_TYPE.B]?: Range | null;
  [RESIZE_TYPE.LT]?: Range | null;
  [RESIZE_TYPE.RT]?: Range | null;
  [RESIZE_TYPE.LB]?: Range | null;
  [RESIZE_TYPE.RB]?: Range | null;
  [CANVAS_STATE.RECT]?: Range | null;
  [CANVAS_STATE.OP]?: CanvasOp | null;
  [CANVAS_STATE.HOVER]?: string | null;
  [CANVAS_STATE.LANDING]?: Point | null;
  [CANVAS_STATE.RESIZE]?: ResizeType | null;
};

最终我又思考了一下,我们在浏览器中进行
DOM
操作的时候,这个
DOM
是真正存在的吗,或者说我们在
PC
上实现窗口管理的时候,这个窗口是真的存在的吗,答案肯定是否定的,虽然我们可以通过系统或者浏览器提供的
API
来非常简单地实现各种操作,但是实际上些内容是系统帮我们绘制出来的,本质上还是图形,事件、状态、碰撞检测等等都是系统模拟出来的,而我们的
Canvas
也拥有类似的图形编程能力。

那么我们当然可以在这里实现类似于
DOM
的能力,因为我想实现的能力似乎本质上就是
DOM
与事件的关联,而
DOM
结构是一种非常成熟的设计了,这其中有一些很棒的能力设计,例如
DOM
的事件流,我们就不需要扁平化地调整每个
Node
的事件,而是只需要保证事件是从
ROOT
节点起始,最终又在
ROOT
上结束即可。并且整个树形结构以及状态是靠用户利用
DOM

API
来实现的,我们管理只需要处理
ROOT
就好了,这样就会很方便,下个阶段的状态管理是准备用这种方式来实现的,那么我们就先实现
Node
基类。

class Node {
  private _range: Range;
  private _parent: Node | null;
  public readonly children: Node[];

  // 尽可能简单地实现事件流
  // 直接通过`bubble`来决定捕获/冒泡
  protected onMouseDown?: (event: MouseEvent) => void;
  protected onMouseUp?: (event: MouseEvent) => void;
  protected onMouseEnter?: (event: MouseEvent) => void;
  protected onMouseLeave?: (event: MouseEvent) => void;

  // `Canvas`绘制节点
  public drawingMask?: (ctx: CanvasRenderingContext2D) => void;

  constructor(range: Range) {
    this.children = [];
    this._range = range;
    this._parent = null;
  }

  // ====== Parent ======
  public get parent() {
    return this._parent;
  }
  public setParent(parent: Node | null) {
    this._parent = parent;
  }

  // ====== Range ======
  public get range() {
    return this._range;
  }
  public setRange(range: Range) {
    this._range = range;
  }

  // ====== DOM OP ======
  public append<T extends Node>(node: T | Empty) {
    // ...
  }
  public removeChild<T extends Node>(node: T | Empty) {
    // ...
  }
  public remove() {
    // ...
  }
  public clearNodes() {
    // ...
  }
}

那么接下来我们只需要定义好类似于
HTML

Body
元素,在这里我们将其设置为
Root
节点,该元素继承了
Node
节点。在这里我们接管了整个编辑器的事件分发,继承于此的事件都可以分发到子节点,例如我们的点选事件,就可以在子节点上设置
MouseDown
事件处理即可。并且在这里我们还需要设计事件分发的能力,我们同样可以实现事件的捕获和冒泡机制,通过栈可以很方便的将事件的触发处理出来。

export class Root extends Node {
  constructor(private editor: Editor, private engine: Canvas) {
    super(Range.from(0, 0));
  }

  public getFlatNode(isEventCall = true): Node[] {
    // 非默认状态下不需要匹配
    if (!this.engine.isDefaultMode()) return [];
    // 事件调用实际顺序 // 渲染顺序则相反
    const flatNodes: Node[] = [...super.getFlatNode(), this];
    return isEventCall ? flatNodes.filter(node => !node.ignoreEvent) : flatNodes;
  }

  public onMouseDown = (e: MouseEvent) => {
    this.editor.canvas.mask.setCursorState(null);
    !e.shiftKey && this.editor.selection.clearActiveDeltas();
  };

  private emit<T extends keyof NodeEvent>(target: Node, type: T, event: NodeEvent[T]) {
    const stack: Node[] = [];
    let node: Node | null = target.parent;
    while (node) {
      stack.push(node);
      node = node.parent;
    }
    // 捕获阶段执行的事件
    for (const node of stack.reverse()) {
      if (!event.capture) break;
      const eventFn = node[type as keyof NodeEvent];
      eventFn && eventFn(event);
    }
    // 节点本身 执行即可
    const eventFn = target[type as keyof NodeEvent];
    eventFn && eventFn(event);
    // 冒泡阶段执行的事件
    for (const node of stack) {
      if (!event.bubble) break;
      const eventFn = node[type as keyof NodeEvent];
      eventFn && eventFn(event);
    }
  }

  private onMouseDownController = (e: globalThis.MouseEvent) => {
    this.cursor = Point.from(e, this.editor);
    // 非默认状态下不执行事件
    if (!this.engine.isDefaultMode()) return void 0;
    // 按事件顺序获取节点
    const flatNode = this.getFlatNode();
    let hit: Node | null = null;
    const point = Point.from(e, this.editor);
    for (const node of flatNode) {
      if (node.range.include(point)) {
        hit = node;
        break;
      }
    }
    hit && this.emit(hit, NODE_EVENT.MOUSE_DOWN, MouseEvent.from(e, this.editor));
  };

  private onMouseMoveBasic = (e: globalThis.MouseEvent) => {
    this.cursor = Point.from(e, this.editor);
    // 非默认状态下不执行事件
    if (!this.engine.isDefaultMode()) return void 0;
    // 按事件顺序获取节点
    const flatNode = this.getFlatNode();
    let next: ElementNode | ResizeNode | null = null;
    const point = Point.from(e, this.editor);
    for (const node of flatNode) {
      // 当前只有`ElementNode`和`ResizeNode`需要触发`Mouse Enter/Leave`事件
      const authorize = node instanceof ElementNode || node instanceof ResizeNode;
      if (authorize && node.range.include(point)) {
        next = node;
        break;
      }
    }
  };
  private onMouseMoveController = throttle(this.onMouseMoveBasic, ...THE_CONFIG);

  private onMouseUpController = (e: globalThis.MouseEvent) => {
    // 非默认状态下不执行事件
    if (!this.engine.isDefaultMode()) return void 0;
    // 按事件顺序获取节点
    const flatNode = this.getFlatNode();
    let hit: Node | null = null;
    const point = Point.from(e, this.editor);
    for (const node of flatNode) {
      if (node.range.include(point)) {
        hit = node;
        break;
      }
    }
    hit && this.emit(hit, NODE_EVENT.MOUSE_UP, MouseEvent.from(e, this.editor));
  };
}

那么接下来,我们只需要定义相关节点类型就可以了,并且通过区分不同类型就可以来实现不同的功能,例如图形绘制使用
ElementNode
节点,调整节点大小使用
ResizeNode
节点,框选内容使用
FrameNode
节点即可,那么在这里我们就先看一下
ElementNode
节点,用来表示实际节点。

class ElementNode extends Node {
  private readonly id: string;
  private isHovering: boolean;

  constructor(private editor: Editor, state: DeltaState) {
    const range = state.toRange();
    super(range);
    this.id = state.id;
    const delta = state.toDelta();
    const rect = delta.getRect();
    this.setZ(rect.z);
    this.isHovering = false;
  }

  protected onMouseDown = (e: MouseEvent) => {
    if (e.shiftKey) {
      this.editor.selection.addActiveDelta(this.id);
    } else {
      this.editor.selection.setActiveDelta(this.id);
    }
  };

  protected onMouseEnter = () => {
    this.isHovering = true;
    if (this.editor.selection.has(this.id)) {
      return void 0;
    }
    this.editor.canvas.mask.drawingEffect(this.range);
  };

  protected onMouseLeave = () => {
    this.isHovering = false;
    if (!this.editor.selection.has(this.id)) {
      this.editor.canvas.mask.drawingEffect(this.range);
    }
  };

  public drawingMask = (ctx: CanvasRenderingContext2D) => {
    if (
      this.isHovering &&
      !this.editor.selection.has(this.id) &&
      !this.editor.state.get(EDITOR_STATE.MOUSE_DOWN)
    ) {
      const { x, y, width, height } = this.range.rect();
      Shape.rect(ctx, {
        x: x,
        y: y,
        width: width,
        height: height,
        borderColor: BLUE_3,
        borderWidth: 1,
      });
    }
  };
}

最后

在这里我们聊了聊如何抽象基本的图形绘制以及状态的管理,因为我们的需求在这里所以我们的图形绘制能力会设计的比较简单,而状态管理则是迭代了三个方案才确定通过轻量
DOM
的方式来实现,那么再往后,我们就需要聊一聊如何实现 层级渲染与事件管理 的能力设计。

前言

其实学习人工智能不难,就跟学习软件开发一样,只是会的人相对少,而一些会的人写文章,做视频又不好好讲。
比如,上来就跟你说要学习张量,或者告诉你张量是向量的多维度等等模式的讲解;目的都是让别人知道他会这个技术,但又不想让你学。
对于学习,多年的学习经验,和无数次的回顾学习过程,都证明了一件事,如果一篇文章,一个视频,一个课程,我没学明白,那问题一定不在我,而是上课的主动或被动的不想让我学会,所以,出问题的一定是学习资料。
比如英语,当真会了以后,再回去看自己之前学过的课,就知道了,那是英语老师没好好教,哪有真正想让你学会英语的人,会告诉你【come=来,out=出去】呀,认认真真按高中大学老师教的方法学习英语。记单词背词组,一百年学不会英语。
比如线性代数,等会了以后,再回去看之前看不懂的课程视频,就知道了,是上课老师估计模糊的关键信息。
学习软件开发,相信大家也都有类似的经验,当你想学一个知识点时,各种搜索,就是看不懂,最后学会的原因只有两种,1,你找到了真正的教你知识的文章,2,你通过搜索的信息,自己悟了出来。这其实就是在证明,绝大多数的文章和视频都不想真正教你,包括正规学校的老师和教材。

入门学习

首先,介绍一下我学习的资料,我通过一通搜索,终于找到了最好的学习资料,该视频是认真教你学习的,唯一的缺点可能就是,上课的人说的是英语,有点印度口音。不过,个人感觉他带点口语,反而更好听懂。
重点关注一下下面单词视频里会多次提到,注意了后就不会被英语卡住了。
numpy:这个单词,这不是个单词,但是是python的库。
vector:向量,下面有解释。
tensor:张量,下面有解释。
gradient:梯度,指的就是我下面的提到的求偏导数。
地址是:
https://www.youtube.com/watch?v=exaWOE8jvy8&list=PLqnslRFeH2UrcDBWF5mfPGpqQDSta6VK4&index=1

安装

pytorch和tensorflow都是做人工智能的,prtorch的函数相对更友好,所以入门更高效。
pytorch官网地址:
https://pytorch.org/get-started/locally/
使用pytorch前,先安装环境,我这里使用了vscode,安装完vscode后,在扩展里把python的包瞎按一些就行。
一般来讲学习都使用cpu版本。安装命令如下:

pip3 install torch torchvision torchaudio

如果使用gpu,安装命令如下:

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124

安装完成后,执行代码,如果执行成功,则安装成功。

import torch
x =torch.ones(1)
print(x)

名词介绍

矩阵:就是我们的表。
向量(vector):这种只有一行/一列的矩阵,叫向量。

1,1,0

二维张量(tensor2D):这有多行多列的矩阵叫二维张量。

1,1,0
1,0,1

三维张量(tensor3D),就是三维数组。
多维张量同多维数组。
为什么叫向量或者张量?我们把[1,1,0]换个想象就行,[1,1,0]是自原点向x=1,y=1,z=0发射出去的线,那[1,1,0]就不在是个数据了,就变成张量了。但本质还是数据。这地方不要细究,理解就可以了,毕竟我们不是研究数学的。

代码入门

使用pytorch进行张量的基础。

import torch
import numpy as np
x=torch.empty(1) # 创建一个只有一个元素的向量(vector),元素值是未定义的,未定义的就是0,打印出来是【0.】,这是因为默认的元素类型是float32
print(x)
x=torch.empty(3)  # 创建一个有三个元素的向量(vector),元素值是未定义的
print(x)
x=torch.empty(3,2)  # 创建一个矩阵,,元素值是未定义的
print(x) 

x=torch.rand(3,2)  # 创建一个3*2的矩阵,并随机赋值
print(x)
x=torch.zeros(3,2)  # 创建一个3*2的矩阵,并赋值0
print(x)

x=torch.ones(2,2)  # 创建一个2*2的矩阵,并赋值1,打印出来是【1.】
print(x)
print("打印类型")
print(x.dtype) #dtype是data type,会打印出元素的类型,打印内容是torch.float32
x=torch.ones(3,3,dtype=torch.int)  # 创建一个3*3的矩阵,并赋值1,打印出来是【1】,这会就不带.了
print(x) 
x=torch.ones(3,3,dtype=torch.double)  # 创建一个3*3的矩阵,并赋值1,打印出来是【1.】,double类型又带.了
print(x)
print(x.size()) #size是个函数,这样打印会打印出toString()的感觉,值是【torch.Size([3, 3])】
print(x.size().numel()) # 元素个数,值是9
x=torch.tensor([2.2,3.1]) # 自定义张量
print(x)
print("===========加法============")
x=torch.ones(3,3,dtype=torch.int)  # 创建一个3*3的矩阵,并赋值1,打印出来是【1】,这会就不带.了
print(x)
y =torch.ones(3,3,dtype=torch.int)   
print(y)
z=x+y #矩阵相加
print(z)
z=torch.add(x,y) #矩阵相加
print(z)
print("===========计算print(y.add_(x))============")
print(y.add_(x)) #把x加到y中去
print("===========减法============")
z=x-y #矩阵相减
print(z)
z=torch.sub(x,y) #矩阵相减
print(z)
print("===========计算print(y.sub_(x))============")
print(y.sub_(x)) #把x减去从y中
print("===========乘法============") #这个乘法是元素相对的相乘,而不是线性代数的 A23*A32
z=x*y #矩阵相乘 
print(z)
z=torch.mul(x,y)
print(z)
print(y.mul_(x))
print("===========除法============")
z=x/y #矩阵相乘除
print(z)
z=torch.div(x,y)
print(z)
print("===========列表============")
x=torch.rand(5,4)  # 创建一个3*2的矩阵,并随机赋值
print(x[:,0]) #打印全部行,但只取第一列
print(x[0,:]) #打印全部列,但只取第一行
print(x[0,0]) #打印i=0 j=0的元素
print(x[1,1].item()) #如果只取一个元素值,则可以取他的真实值
print(x)
print("===========view可以resize tensor============")
x=torch.rand(5,4) 
y=x.view(20) #返回一个新的张量,这个是返回一个1行的20个元素的张量
print(y)
y=x.view(-1,10)
print(y) # 这个是返回2行,每行10个,他做了自动适配
print(y.size())#输出size
#print(x.view(-1,7)) # 这个自动适配不了,因为不能被7整除
print("===========numpy numpy只能在cpu上使用,不能在gpu上使用============")
a=torch.ones(5) #行向量,值是1,元素是5
b=a.numpy() #返回 numpy.ndarray类型的numpy下的张量,相当于转了类型,用于计算,该函数有参数 默认是false,表示使用cpu
print(b,type(b))
 #这里虽然a转了类型到b,但b和a是对象封装,引用地址一样 所以当我们给a+1时,b也会+1
a.add_(1)
print(a)
print(b)#虽然a b类型不一样,但值都改变了
print("===========从numpy.ndarray转成tensor张量的方式============")
a = np.ones(5) #行向量 5元素 值是1
b =torch.from_numpy(a) #numpy的ndarray转tensor 同样是装箱拆箱 修改a的值 b也会变
a+=1
print(b)
print(a)
print("===========gpu============")
if(torch.cuda.is_available()):
    #CUDA 是指 NVIDIA 的并行计算平台和编程模型,它利用图形处理单元 (GPU) 的处理能力来加速计算密集型任务
    device =torch.device("cuda") #获取cuda驱动
    x=torch.ones(5,device=device)#创建时指定了使用cpu的内存
    y=torch.ones(5)#创建时使用cpu的内存
    y=y.to(device)#将y转到gpu
    z=x+y #这个操作是在gpu的内存上进行了
    #z.numpy()#这个不能执行,因为z在gpu的内存上
    z =z.to("cpu") #转回到cpu
else:
    print("this is cpu")

requires_grad例子。
这里要点高数基础。
首先是导数,这个大家忘了的可以百度一下。
偏导数:这个就是f(x,y)=x+y这样的函数求导,只是对x求导时,把y当常量c,反之亦然。

print("===========requires_grad 例子1============")
#使用自动微分(autograd)
x=torch.ones(5,requires_grad=True) #默认requires_grad是false  1,计算梯度:requires_grad 是一个布尔参数,用于指定一个张量是否需要计算梯度 2,自动求导:使用 requires_grad=True 的张量进行的所有操作都会被记录,以便稍后使用 backward() 方法进行自动求导。
print(x)
# 对张量进行一些操作
y = x + 2
print(y)
# 再进行一些操作
z = y * y * 3
print("======分割线1=====")
print(z)
out = z.mean() #是一个张量操作,它计算张量 z 的所有元素的平均值。 看做f(x)=(x1+x2+x3+x4)/4 然后对每个x求偏导,在把值带回去
print("======分割线2=====")
print(out)
# 进行反向传播,计算梯度
out.backward()
print("======分割线3=====")
print(x.grad)  # 输出x的梯度
print("===========requires_grad 例子2============")
# 创建一个张量,并指定需要计算梯度
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) 
# 定义一个标量函数 平方在求和 相当于函数 f()= x1²+ x2²,+ x3²
y = x.pow(2).sum() 
# 进行反向传播,计算梯度 x`的梯度,它对应于函数 f(x1,x2,x3)= x1²+ x2²+ x3², 三个偏导数就是 2x1,2x2,2x3,带入tensor的值 即'[2x1,2x2,2x3]
y.backward() 
# 输出 x 的梯度
print(x.grad)  # 输出 tensor([2., 4., 6.])
print("===========requires_grad 例子3============")
# 创建一个张量,并指定需要计算梯度
x = torch.tensor([[1.0, 2.0],[4.0, 5.0]], requires_grad=True) 
# 定义一个标量函数 平方在求和 相当于函数 f()= x1²+ x2²+ x3²+ x4²
y = x.pow(2).sum() 
# 进行反向传播,计算梯度 x`的梯度,这里是2*2矩阵,但计算的时候,就按元素个数算x,没有行列ij。
# 它对应于函数 f(x1,x2,x3,x4)= x1²+ x2²,+ x3²+ x4², 四个偏导数就是 2x1,2x2,2x3,2x4,带入tensor的值 即'[2x1,2x2,2x4,2x5]
y.backward() 
# 输出 x 的梯度
print(x.grad)  # 输出 tensor([[2, 4][8,10]])

基础学习就先到这。


注:此文章为原创,任何形式的转载都请联系作者获得授权并注明出处!



若您觉得这篇文章还不错,请点击下方的【推荐】,非常感谢!

https://www.cnblogs.com/kiba/p/18346596

前言

IoTClient 是一个针对物联网 (IoT) 领域的开源客户端库,它主要用于实现与各种工业设备之间的通信。这个库是用 C# 编写的,并且基于 .NET Standard 2.0,这意味着可以用于多个版本的.NET,包括 .NET Framework、.NET Core、.NET 5 及以上版本,以及 Xamarin 和 UWP。

项目介绍

IoTClient 是一款基于 .NET Standard 2.0 的开源免费客户端库,适用于跨平台的 .NET 开发,可在 Windows、Linux 乃至树莓派等平台上运行。

该组件支持主流的工业通讯协议,包括 PLC 通信(如 Siemens、Mitsubishi、Omron、Allen Bradley 等)、ModBus(TCP/RTU/ASCII)以及 Bacnet 等。

IoTClient 采用 MIT 许可证发布,您可以自由地修改和进行商业使用(请注意在商业使用前做好充分的评估和测试)。

项目特点

支持多种通信协议

  • ModBus
    :支持 ModBus TCP、ModBus RTU、ModBus ASCII 以及 ModBus RTU over TCP。
  • Bacnet
    :支持 Bacnet 协议。
  • PLC通信
    :支持 Siemens、Mitsubishi、Omron、Allen Bradley 等主流品牌的 PLC 通信。

跨平台

  • 可以在 Windows、Linux 上运行。
  • 支持在 Raspberry Pi(树莓派)这样的小型设备上部署。

开源和免费

  • 使用 MIT 许可证发布,一个非常宽松的许可证,允许自由使用、修改和分发软件。

NuGet 包

  • 可以通过 NuGet 包管理器安装到的项目中。

项目使用

1、引用组件

Nuget 安装

Install-Package IoTClient

或图形化安装

2、ModBusTcp读写操作

//1、实例化客户端 - 输入正确的IP和端口
ModBusTcpClient client = new ModBusTcpClient("127.0.0.1", 502);//2、写操作 - 参数依次是:地址 、值 、站号 、功能码
client.Write("4", (short)33, 2, 16);//2.1、【注意】写入数据的时候需要明确数据类型
client.Write("0", (short)33, 2, 16);    //写入short类型数值
client.Write("4", (ushort)33, 2, 16);   //写入ushort类型数值
client.Write("8", (int)33, 2, 16);      //写入int类型数值
client.Write("12", (uint)33, 2, 16);    //写入uint类型数值
client.Write("16", (long)33, 2, 16);    //写入long类型数值
client.Write("20", (ulong)33, 2, 16);   //写入ulong类型数值
client.Write("24", (float)33, 2, 16);   //写入float类型数值
client.Write("28", (double)33, 2, 16);  //写入double类型数值
client.Write("32", true, 2, 5);         //写入线圈类型值
client.Write("100", "orderCode", stationNumber);  //写入字符串//3、读操作 - 参数依次是:地址 、站号 、功能码
var value = client.ReadInt16("4", 2, 3).Value;//3.1、其他类型数据读取
client.ReadInt16("0", stationNumber, 3);    //short类型数据读取
client.ReadUInt16("4", stationNumber, 3);   //ushort类型数据读取
client.ReadInt32("8", stationNumber, 3);    //int类型数据读取
client.ReadUInt32("12", stationNumber, 3);  //uint类型数据读取
client.ReadInt64("16", stationNumber, 3);   //long类型数据读取
client.ReadUInt64("20", stationNumber, 3);  //ulong类型数据读取
client.ReadFloat("24", stationNumber, 3);   //float类型数据读取
client.ReadDouble("28", stationNumber, 3);  //double类型数据读取
client.ReadCoil("32", stationNumber, 1);    //线圈类型数据读取
client.ReadDiscrete("32", stationNumber, 2);//离散类型数据读取
client.ReadString("100", stationNumber,10); //读取字符串//4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。
client.Open();//5、读写操作都会返回操作结果对象Result
var result = client.ReadInt16("4", 2, 3);//5.1 读取是否成功(true或false)
var isSucceed =result.IsSucceed;//5.2 读取失败的异常信息
var errMsg =result.Err;//5.3 读取操作实际发送的请求报文
var requst  =result.Requst;//5.4 读取操作服务端响应的报文
var response =result.Response;//5.5 读取到的值
var value3 =result.Value;//6、批量读取
var list = new List<ModBusInput>();
list.Add(
newModBusInput()
{
Address
= "2",
DataType
=DataTypeEnum.Int16,
FunctionCode
= 3,
StationNumber
= 1});
list.Add(
newModBusInput()
{
Address
= "2",
DataType
=DataTypeEnum.Int16,
FunctionCode
= 4,
StationNumber
= 1});
list.Add(
newModBusInput()
{
Address
= "199",
DataType
=DataTypeEnum.Int16,
FunctionCode
= 3,
StationNumber
= 1});var result =client.BatchRead(list);//7、构造函数其他参数//IP、端口、超时时间、大小端设置 ModBusTcpClient client = new ModBusTcpClient("127.0.0.1", 502, 1500, EndianFormat.ABCD);

3、ModBusRtu读写操作

//实例化客户端 - [COM端口名称,波特率,数据位,停止位,奇偶校验]
ModBusRtuClient client = new ModBusRtuClient("COM3", 9600, 8, StopBits.One, Parity.None);//其他读写操作和ModBusTcpClient的读写操作一致

4、ModBusAscii读写操作

//实例化客户端 - [COM端口名称,波特率,数据位,停止位,奇偶校验]
ModbusAsciiClient client = new ModbusAsciiClient("COM3", 9600, 8, StopBits.One, Parity.None);//其他读写操作和ModBusTcpClient的读写操作一致

5、ModbusRtuOverTcp读写操作

//串口透传 即:用Tcp的方式发送Rtu格式报文//实例化客户端 - IP、端口、超时时间、大小端设置
ModbusRtuOverTcpClient client = new ModbusRtuOverTcpClient("127.0.0.1", 502, 1500, EndianFormat.ABCD);//其他读写操作和ModBusTcpClient的读写操作一致

6、SiemensClient(西门子)读写操作

//1、实例化客户端 - 输入型号、IP和端口//其他型号:SiemensVersion.S7_200、SiemensVersion.S7_300、SiemensVersion.S7_400、SiemensVersion.S7_1200、SiemensVersion.S7_1500
SiemensClient client = new SiemensClient(SiemensVersion.S7_200Smart, "127.0.0.1",102);//2、写操作
client.Write("Q1.3", true);
client.Write(
"V2205", (short)11);
client.Write(
"V2209", 33);
client.Write(
"V2305", "orderCode"); //写入字符串//3、读操作 var value1 = client.ReadBoolean("Q1.3").Value;var value2 = client.ReadInt16("V2205").Value;var value3 = client.ReadInt32("V2209").Value;var value4 = client.ReadString("V2305").Value; //读取字符串//4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。 client.Open();//5、读写操作都会返回操作结果对象Result var result = client.ReadInt16("V2205");//5.1 读取是否成功(true或false) var isSucceed =result.IsSucceed;//5.2 读取失败的异常信息 var errMsg =result.Err;//5.3 读取操作实际发送的请求报文 var requst =result.Requst;//5.4 读取操作服务端响应的报文 var response =result.Response;//5.5 读取到的值 var value4 = result.Value;

7、SiemensClient最佳实践

1、什么时候不要主动Open
西门子plc一般最多允许8个长连接。所以当连接数不够用的时候或者做测试的时候就不要主动Open,这样组件会自动Open并即时Close。
2、什么时候主动Open
当长连接数量还够用,且想要提升读写性能。
3、除了主动Open连接,还可以通过批量读写,大幅提升读写性能。//批量读取 Dictionary<string, DataTypeEnum> addresses = new Dictionary<string, DataTypeEnum>();
addresses.Add(
"DB4.24", DataTypeEnum.Float);
addresses.Add(
"DB1.434.0", DataTypeEnum.Bool);
addresses.Add(
"V4109", DataTypeEnum.Byte);
...
var result =client.BatchRead(addresses);//批量写入 Dictionary<string, object> addresses = new Dictionary<string, object>();
addresses.Add(
"DB4.24", (float)1);
addresses.Add(
"DB4.0", (float)2);
addresses.Add(
"DB1.434.0", true);
...
var result =client.BatchWrite(addresses);4、【注意】写入数据的时候需要明确数据类型
client.Write(
"DB4.12", 9); //写入的是int类型 client.Write("DB4.12", (float)9); //写入的是float类型 5、SiemensClient是线程安全类
由于plc长连接有限,SiemensClient被设计成线程安全类。可以把SiemensClient设置成单例,在多个线程之间使用SiemensClient的实例读写操作plc。

8、MitsubishiClient

//1、实例化客户端 - 输入正确的IP和端口
MitsubishiClient client = new MitsubishiClient(MitsubishiVersion.Qna_3E, "127.0.0.1",6000);//2、写操作
client.Write("M100", true);
client.Write(
"D200", (short)11);
client.Write(
"D210", 33);//3、读操作 var value1 = client.ReadBoolean("M100").Value;var value2 = client.ReadInt16("D200").Value;var value3 = client.ReadInt32("D210").Value;//4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。 client.Open();//5、读写操作都会返回操作结果对象Result var result = client.ReadInt16("D210");//5.1 读取是否成功(true或false) var isSucceed =result.IsSucceed;//5.2 读取失败的异常信息 var errMsg =result.Err;//5.3 读取操作实际发送的请求报文 var requst =result.Requst;//5.4 读取操作服务端响应的报文 var response =result.Response;//5.5 读取到的值 var value4 = result.Value;

9、OmronFinsClient(欧姆龙)读写操作

//1、实例化客户端 - 输入正确的IP和端口
OmronFinsClient client = new OmronFinsClient("127.0.0.1",6000);//2、写操作
client.Write("M100", true);
client.Write(
"D200", (short)11);
client.Write(
"D210", 33);//3、读操作 var value1 = client.ReadBoolean("M100").Value;var value2 = client.ReadInt16("D200").Value;var value3 = client.ReadInt32("D210").Value;//4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。 client.Open();//5、读写操作都会返回操作结果对象Result var result = client.ReadInt16("D210");//5.1 读取是否成功(true或false) var isSucceed =result.IsSucceed;//5.2 读取失败的异常信息 var errMsg =result.Err;//5.3 读取操作实际发送的请求报文 var requst =result.Requst;//5.4 读取操作服务端响应的报文 var response =result.Response;//5.5 读取到的值 var value4 = result.Value;

10、AllenBradleyClient(罗克韦尔)读写操作

//1、实例化客户端 - 输入正确的IP和端口
AllenBradleyClient client = new AllenBradleyClient("127.0.0.1",44818);//2、写操作
client.Write("A1", (short)11);//3、读操作
var value = client.ReadInt16("A1").Value;//4、如果没有主动Open,则会每次读写操作的时候自动打开自动和关闭连接,这样会使读写效率大大减低。所以建议手动Open和Close。
client.Open();//5、读写操作都会返回操作结果对象Result
var result = client.ReadInt16("A1");//5.1 读取是否成功(true或false)
var isSucceed =result.IsSucceed;//5.2 读取失败的异常信息
var errMsg =result.Err;//5.3 读取操作实际发送的请求报文
var requst  =result.Requst;//5.4 读取操作服务端响应的报文
var response =result.Response;//5.5 读取到的值
var value4 = result.Value;

IoTClient 库项目

IoTClient Tool 桌面程序工具

1、开源地址 https://github.com/zhaopeiym/IoTClient.Examples

2、可用来测试PLC和相关协议的通信

3、可作为IoTClient库使用例子。

4、IoTClient Tool效果图

iotgateway

1、开源地址 https://gitee.com/iioter/iotgateway

2、在线体验 http://online.iotgateway.net 用户名:admin 密码:iotgateway.net

3、基于.NET 5 的跨平台物联网网关。

4、通过可视化配置,轻松的连接到任何设备和系统(如PLC、扫码枪、CNC、数据库、串口设备、上位机、OPC Server、OPC UA Server、Mqtt Server等)

5、提供简单的驱动开发接口;当然也可以进行边缘计算。

6、系统页面展示


项目商用效果

1、 能源管理-现场-单项目

移动端

2、越邦智能分拣系统


项目地址

github:
https://github.com/zhaopeiym/IoTClient

gitee:
https://gitee.com/zhaopeiym/IoTClient

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!

VMware vSphere 8.0 U3 发布了一个非常有意义的功能叫内存分层(Memory Tiering),以利用基于 PCIe 的 NVMe 设备充当第二层(辅助)内存,从而使 ESXi 主机的可用物理内存(RAM)增加。从本质上讲,内存分层就是利用较便宜的 NVMe 设备充当物理内存,以此增加内存的总容量和工作负载的可用量,同时降低总体拥有成本(TCO)。

为什么会需要内存分层?会不会有很大性能瓶颈?原因有很多,比如内存成本,如今对于内存容量及性能的需求与 CPU 的需求其实是不平衡的,这可能会因内存原因导致许多环境出现诸多限制。对于性能的影响肯定是会有的,但是随着技术的发展,慢慢的也会得到控制。内存分层对应用程序来说是完全透明的,可以用于所有不同类型的工作负载。虚拟机管理程序负责内存管理,因此它知道哪些页面是热的,哪些页面是冷的,也就是说,它可以确定哪些页面用于哪个层并同时保持性能。

当前对于内存分层的配置,VMware 建议 NVMe 存储容量与物理内存容量的分配比率为 1:4,也就是 25%。意思就是,如果物理内存为 100 GB,那用于内存分层的 NVMe 存储建议为 25 GB,合在一起可以使用的内存容量为 125 GB,这样既可以增加物理内存的容量,也能减少因内存分层所带来的性能影响。当然这个比率只是官方推荐的也是默认的,这个值可以修改,你可以使用 1~400 的值来设置物理内存与 NVMe 存储的百分比。更多内容和细节请查看 VMware
KB 95944
知识库文章底部的内存分层技术指南文档。

现在,内存分层(Memory Tiering)技术还只是预览版,可以在实验室或者测试环境评估该功能,只能通过 ESXCLI 或者 PowerCLI 命令行进行配置,不过在未来的发行版本中,也许可以直接在 UI 管理界面进行应用。下面来看看具体的配置过程。

首先,ESXi 主机必须安装 8.0 U3 及以上版本来支持内存分层功能,请记住这里现在的物理内存“容量”。

然后,使用了一块 Samsung 970 EVO 250 GB 的 NVMe 硬盘进行测试,请记住这里的“路径”以及“容量”。

1.SSH 登录到 ESXi 主机。

ssh z4g4.mulab.local

2.运行 ESXCLI 命令启用内存分层功能。

esxcli system settings kernel set -s MemoryTiering -v TRUE

3.创建特定 NVMe 设备以用于内存分层。

esxcli system tierdevice create -d /vmfs/devices/disks/t10.NVMe____Samsung_SSD_970_EV0_250GB_______________5C71B5815A382500

4.查看用于内存分层的 NVMe 设备。

esxcli system tierdevice list

5.配置用于内存分层的 NVMe 设备与物理内存的百分比。

esxcli system settings advanced set -o /Mem/TierNvmePct -i 100

6.ESXi 主机内存分层功能配置过程。

7.完成配置后,重新启动 ESXi 主机使配置生效,再次查看 ESXi 主机的内存!当前内存容量=物理内存容量+NVMe 存储容量。

是不是很神奇?你的下一代内存何必是内存?还不赶紧用起来?内存分层都来了,内存池化还远吗?!